diff --git a/README.md b/README.md
index 5fd7dd19..7e593e23 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Make sure you have **Java 8** or higher.
```groovy
// gradle.build
dependencies {
- implementation 'com.expediagroup:lodging-connectivity-sdk:1.0.7-SNAPSHOT'
+ implementation 'com.expediagroup:lodging-connectivity-sdk:1.0.6-SNAPSHOT'
}
```
@@ -29,7 +29,7 @@ dependencies {
com.expediagroup
lodging-connectivity-sdk
- 1.0.7-SNAPSHOT
+ 1.0.6-SNAPSHOT
```
diff --git a/apollo-compiler-plugin/build.gradle b/apollo-compiler-plugin/build.gradle
index 28f248dc..bb077351 100644
--- a/apollo-compiler-plugin/build.gradle
+++ b/apollo-compiler-plugin/build.gradle
@@ -1,12 +1,3 @@
-plugins {
- id 'org.jetbrains.kotlin.jvm' version '2.0.21'
-}
-
-kotlin {
- jvmToolchain(8)
-}
-
dependencies {
- // Add apollo-compiler as a dependency
- implementation("com.apollographql.apollo:apollo-compiler:4.1.0")
+ implementation 'com.apollographql.apollo:apollo-compiler:4.1.0'
}
diff --git a/build.gradle b/build.gradle
index 97cd1fdd..0b11f37e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,22 @@
+plugins {
+ id 'java'
+ id 'org.jetbrains.kotlin.jvm' version '2.0.21'
+}
+
subprojects {
/* Shared plugins between all modules */
apply plugin: 'java'
+ apply plugin: 'org.jetbrains.kotlin.jvm'
/* Shared repositories between all modules */
repositories {
mavenCentral()
}
+ kotlin {
+ jvmToolchain(8)
+ }
+
java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_1_8
diff --git a/code/build.gradle b/code/build.gradle
index 16f0f977..756319e2 100644
--- a/code/build.gradle
+++ b/code/build.gradle
@@ -1,5 +1,4 @@
plugins {
- id 'org.jetbrains.kotlin.jvm' version '2.0.21'
id 'org.jetbrains.dokka' version '1.9.20'
id 'com.apollographql.apollo' version '4.1.0'
@@ -8,8 +7,14 @@ plugins {
id 'signing'
}
-kotlin {
- jvmToolchain(8)
+jar {
+ manifest {
+ attributes(
+ "version": version,
+ "artifactId": rootProject.name,
+ "userAgentPrefix": "expediagroup-sdk-java-${rootProject.name.replaceFirst("-sdk", "")}"
+ )
+ }
}
dependencies {
@@ -19,29 +24,19 @@ dependencies {
/* Dokka */
dokkaHtmlPlugin 'org.jetbrains.dokka:versioning-plugin:1.9.20'
- /* Apollo Kotlin */
- api 'com.apollographql.apollo:apollo-runtime:4.1.0'
+ /* Apollo */
+ api 'com.apollographql.java:client:0.0.2'
implementation 'com.apollographql.adapters:apollo-adapters-core:0.0.4'
- implementation 'com.apollographql.ktor:apollo-engine-ktor:0.0.2'
+
/* EG SDK Core */
- implementation 'io.ktor:ktor-client-core:2.3.13'
- implementation 'io.ktor:ktor-client-auth-jvm:2.3.13'
- implementation 'io.ktor:ktor-http:2.3.13'
- implementation 'io.ktor:ktor-client-okhttp:2.3.13'
- implementation 'io.ktor:ktor-client-content-negotiation-jvm:2.3.13'
- implementation 'io.ktor:ktor-serialization-jackson-jvm:2.3.13'
- implementation 'io.ktor:ktor-client-logging-jvm:2.3.13'
- implementation 'io.ktor:ktor-client-encoding:2.3.13'
- implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
- implementation 'org.jetbrains.kotlinx:atomicfu-jvm:0.26.0'
- implementation 'javax.validation:validation-api:2.0.1.Final'
- implementation 'org.hibernate.validator:hibernate-validator:6.2.5.Final'
- implementation 'jakarta.validation:jakarta.validation-api:2.0.2'
+ implementation 'com.ebay.ejmask:ejmask-api:1.0.3'
+ implementation 'com.ebay.ejmask:ejmask-extensions:1.0.3'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.1'
+ implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.18.1'
+ implementation 'org.slf4j:slf4j-api:2.0.16'
}
apply from: "tasks-gradle/apollo.gradle"
apply from: "tasks-gradle/publishing.gradle"
apply from: "tasks-gradle/dokka.gradle"
-
-
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptor.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptor.kt
new file mode 100644
index 00000000..01566aeb
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationInterceptor.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.authentication.bearer
+
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.Response
+import com.expediagroup.sdk.core.interceptor.Interceptor
+import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException
+import java.io.IOException
+
+/**
+ * An interceptor that handles bearer token-based authentication for HTTP requests.
+ *
+ * This interceptor ensures that an up-to-date bearer token is added to the `Authorization` header of each HTTP request.
+ * It manages token expiration and re-authentication automatically. If the token is about to expire, the interceptor
+ * synchronously refreshes it before proceeding with the request.
+ *
+ * @param authManager The [BearerAuthenticationManager] used for making authentication requests execution and parsing.
+ */
+class BearerAuthenticationInterceptor(
+ private val authManager: BearerAuthenticationManager
+) : Interceptor {
+
+ private val lock = Any()
+
+ /**
+ * Intercepts the HTTP request, adding a bearer token to the `Authorization` header.
+ *
+ * This method checks if the token needs to be refreshed and does so if necessary. It excludes
+ * requests targeting the `authUrl` from this behavior to avoid recursive authentication requests.
+ *
+ * @param chain The [Interceptor.Chain] responsible for managing the request and its progression.
+ * @return The [Response] resulting from the executed request.
+ * @throws ExpediaGroupAuthException If authentication fails due to invalid credentials or server errors
+ */
+ @Throws(ExpediaGroupAuthException::class)
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+
+ if (isAuthenticationRequest(request)) {
+ return chain.proceed(request)
+ }
+
+ ensureValidAuthentication()
+
+ val authorizedRequest = request.newBuilder()
+ .addHeader("Authorization", authManager.getAuthorizationHeaderValue())
+ .build()
+
+ return chain.proceed(authorizedRequest)
+ }
+
+ /**
+ * Checks if the given request is for authentication.
+ */
+ private fun isAuthenticationRequest(request: Request): Boolean = request.url.toString() == authManager.authUrl
+
+ /**
+ * Ensures there is a valid authentication token available.
+ * If needed, authenticates under a synchronization lock to prevent multiple simultaneous authentications.
+ *
+ * @throws ExpediaGroupAuthException If authentication fails
+ */
+ @Throws(ExpediaGroupAuthException::class)
+ private fun ensureValidAuthentication() {
+ try {
+ if (authManager.isTokenAboutToExpire()) {
+ synchronized(lock) {
+ if (authManager.isTokenAboutToExpire()) {
+ authManager.authenticate()
+ }
+ }
+ }
+ } catch (e: IOException) {
+ throw ExpediaGroupAuthException("Failed to authenticate", e)
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManager.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManager.kt
new file mode 100644
index 00000000..67fc1ca1
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerAuthenticationManager.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.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.MediaType
+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.logging.common.LoggerDecorator
+import com.expediagroup.sdk.core.logging.common.RequestLogger
+import com.expediagroup.sdk.core.logging.common.ResponseLogger
+import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupResponseParsingException
+import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException
+import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupNetworkException
+import org.slf4j.LoggerFactory
+
+/**
+ * Manages bearer token authentication for HTTP requests.
+ *
+ * The `BearerAuthenticationManager` handles the lifecycle of bearer tokens, including retrieval, storage,
+ * and validation. It interacts with an authentication server to fetch tokens using client credentials,
+ * ensures tokens are refreshed when necessary, and provides them in the required format for authorization headers.
+ *
+ * @param transport The [Transport] used to execute authentication requests.
+ * @param authUrl The URL of the authentication server's endpoint to obtain bearer tokens.
+ * @param credentials The [Credentials] containing the client key and secret used for authentication.
+ */
+class BearerAuthenticationManager(
+ val authUrl: String,
+ private val transport: Transport,
+ private val credentials: Credentials
+) : AuthenticationManager {
+
+ @Volatile
+ private var bearerTokenStorage = BearerTokenStorage.empty
+
+ /**
+ * Initiates authentication to obtain a new bearer token.
+ *
+ * This method sends a request to the authentication server, parses the response, and
+ * stores the token for future use.
+ *
+ * @throws ExpediaGroupAuthException If the authentication request fails.
+ * @throws ExpediaGroupResponseParsingException If the response cannot be parsed.
+ */
+ @Throws(ExpediaGroupAuthException::class, ExpediaGroupResponseParsingException::class)
+ override fun authenticate() {
+ clearAuthentication()
+ .let {
+ buildAuthenticationRequest().also {
+ RequestLogger.log(logger, it, "Authentication")
+ }
+ }.let {
+ executeAuthenticationRequest(it).also {
+ ResponseLogger.log(logger, it, "Authentication")
+ }
+ }.let {
+ TokenResponse.parse(it)
+ }.also {
+ storeToken(it)
+ }
+ }
+
+ /**
+ * Checks if the current bearer token is about to expire and needs renewal.
+ *
+ * @return `true` if the token is near expiration, `false` otherwise.
+ */
+ fun isTokenAboutToExpire(): Boolean = run {
+ bearerTokenStorage.isAboutToExpire()
+ }
+
+ /**
+ * Clears the stored authentication token.
+ *
+ * This method resets the internal token storage, effectively invalidating the current session.
+ */
+ override fun clearAuthentication() = run {
+ bearerTokenStorage = BearerTokenStorage.empty
+ }
+
+ /**
+ * Retrieves the stored token formatted as an `Authorization` header value.
+ *
+ * @return The token in the format `Bearer ` for use in HTTP headers.
+ */
+ fun getAuthorizationHeaderValue(): String = run {
+ bearerTokenStorage.getAuthorizationHeaderValue()
+ }
+
+ /**
+ * Creates an HTTP request to fetch a new bearer token from the authentication server.
+ *
+ * @return A [Request] object configured with the necessary headers and parameters.
+ */
+ private fun buildAuthenticationRequest(): Request = run {
+ Request.Builder()
+ .url(authUrl)
+ .method("POST", RequestBody.create(mapOf("grant_type" to "client_credentials")))
+ .header("Authorization", credentials.encodeBasic())
+ .header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED.toString())
+ .build()
+ }
+
+ /**
+ * Executes the authentication request and validates the response.
+ *
+ * @param request The [Request] object to be executed.
+ * @return The [Response] received from the server.
+ * @throws ExpediaGroupAuthException If the server responds with an error.
+ */
+ @Throws(ExpediaGroupAuthException::class, ExpediaGroupNetworkException::class)
+ private fun executeAuthenticationRequest(request: Request): Response = run {
+ transport.execute(request).apply {
+ if (!this.isSuccessful) {
+ throw ExpediaGroupAuthException(this.code, "Authentication failed")
+ }
+ }
+ }
+
+ /**
+ * Stores the retrieved token in internal storage for subsequent use.
+ *
+ * @param tokenResponse The [TokenResponse] containing the token and its expiration time.
+ */
+ private fun storeToken(tokenResponse: TokenResponse) = run {
+ bearerTokenStorage = BearerTokenStorage.create(
+ accessToken = tokenResponse.accessToken,
+ expiresIn = tokenResponse.expiresIn
+ )
+ }
+
+ private companion object {
+ private val logger = LoggerDecorator(LoggerFactory.getLogger(this::class.java.enclosingClass))
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorage.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorage.kt
new file mode 100644
index 00000000..6887ff3b
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/BearerTokenStorage.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.authentication.bearer
+
+import java.time.Clock
+import java.time.Instant
+
+/**
+ * Stores and manages a bearer token and its expiration details.
+ *
+ * The `BearerTokenStorage` class is responsible for encapsulating the bearer token along with its
+ * expiration time. It provides utilities to check if the token is about to expire and to format
+ * the token as an `Authorization` header value.
+ *
+ * @param accessToken The bearer token.
+ * @param expiresIn The time in seconds until the token expires, relative to when it was issued.
+ * @param expirationBufferSeconds The number of seconds before the token's expiration time that it is considered "about to expire".
+ * @param clock The clock to use for time-based operations. Defaults to system clock.
+ */
+class BearerTokenStorage private constructor(
+ val accessToken: String,
+ val expiresIn: Long,
+ private val expirationBufferSeconds: Long,
+ private val clock: Clock,
+ private val expiryInstant: Instant
+) {
+
+ /**
+ * Checks if the bearer token is about to expire.
+ *
+ * A token is considered "about to expire" if the current time is within the configured buffer
+ * of the token's expiration time.
+ *
+ * @return `true` if the token is about to expire; `false` otherwise.
+ */
+ fun isAboutToExpire(): Boolean = run {
+ Instant.now(clock).isAfter(expiryInstant.minusSeconds(expirationBufferSeconds))
+ }
+
+ /**
+ * Formats the bearer token as an `Authorization` header value.
+ *
+ * @return The token in the format `Bearer `.
+ */
+ fun getAuthorizationHeaderValue(): String = "Bearer $accessToken"
+
+ companion object {
+ private const val DEFAULT_EXPIRATION_BUFFER_SECONDS = 60L
+
+ /**
+ * Creates an empty bearer token storage instance.
+ * This instance will always report as expired.
+ */
+ val empty: BearerTokenStorage = create("", -1)
+
+ /**
+ * Creates a new bearer token storage instance with default settings.
+ *
+ * @param accessToken The bearer token
+ * @param expiresIn The time in seconds until the token expires
+ * @param expirationBufferSeconds Optional buffer time before expiration. Defaults to 60 seconds.
+ * @param clock Optional clock for time operations. Defaults to system clock.
+ * @return A new BearerTokenStorage instance
+ */
+ fun create(
+ accessToken: String,
+ expiresIn: Long,
+ expirationBufferSeconds: Long = DEFAULT_EXPIRATION_BUFFER_SECONDS,
+ clock: Clock = Clock.systemUTC()
+ ): BearerTokenStorage {
+ val expiryInstant = if (expiresIn >= 0) {
+ Instant.now(clock).plusSeconds(expiresIn)
+ } else {
+ Instant.EPOCH
+ }
+
+ return BearerTokenStorage(
+ accessToken = accessToken,
+ expiresIn = expiresIn,
+ expirationBufferSeconds = expirationBufferSeconds,
+ clock = clock,
+ expiryInstant = expiryInstant
+ )
+ }
+ }
+
+ override fun toString(): String {
+ return "BearerTokenStorage(expiresIn=$expiresIn, expirationBufferSeconds=$expirationBufferSeconds, expiryInstant=$expiryInstant)"
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponse.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponse.kt
new file mode 100644
index 00000000..04f5e786
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/bearer/TokenResponse.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.authentication.bearer
+
+import com.expediagroup.sdk.core.extension.getOrThrow
+import com.expediagroup.sdk.core.http.Response
+import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupResponseParsingException
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.registerKotlinModule
+
+/**
+ * Represents the response from an authentication server containing a bearer token and its expiration details.
+ *
+ * The `TokenResponse` class is used to deserialize the response from an authentication server. It includes
+ * the bearer token and the duration (in seconds) until the token expires.
+ *
+ * @param accessToken The bearer token issued by the authentication server.
+ * @param expiresIn The time in seconds until the token expires, starting from when it was issued.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+data class TokenResponse(
+ @JsonProperty("access_token") val accessToken: String,
+ @JsonProperty("expires_in") val expiresIn: Long
+) {
+ companion object {
+ private val objectMapper = ObjectMapper()
+ .registerKotlinModule()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ /**
+ * Parses the response from the authentication server to extract token details.
+ *
+ * @param response The [Response] from the authentication server.
+ * @return A [TokenResponse] object containing the token and its metadata.
+ * @throws ExpediaGroupResponseParsingException If the response cannot be parsed.
+ */
+ @Throws(ExpediaGroupResponseParsingException::class)
+ fun parse(response: Response): TokenResponse {
+ val responseBody = response.body.getOrThrow {
+ ExpediaGroupResponseParsingException("Authenticate response body is empty or cannot be parsed")
+ }
+
+ val responseString = responseBody.source().use {
+ it.readString(responseBody.mediaType()?.charset ?: Charsets.UTF_8)
+ }
+
+ return try {
+ objectMapper.readValue(responseString, TokenResponse::class.java)
+ } catch (e: Exception) {
+ throw ExpediaGroupResponseParsingException("Failed to parse authentication response", e)
+ }
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/common/AuthenticationManager.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/common/AuthenticationManager.kt
new file mode 100644
index 00000000..69b5cc9b
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/common/AuthenticationManager.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.authentication.common
+
+import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupResponseParsingException
+import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException
+import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupNetworkException
+
+/**
+ * Defines the contract for managing authentication within the SDK.
+ *
+ * An `AuthenticationManager` is responsible for handling the process of authenticating with an external
+ * service or API and maintaining the authentication state. Implementations should handle token lifecycle,
+ * including acquisition, storage, and renewal.
+ */
+interface AuthenticationManager {
+ /**
+ * Performs the authentication process, obtaining the necessary credentials or tokens.
+ *
+ * This method is responsible for executing the authentication logic, such as sending requests to an
+ * authentication server, handling the response, and storing the retrieved credentials or tokens for future use.
+ *
+ * @throws ExpediaGroupAuthException If authentication fails due to invalid credentials or server errors
+ * @throws ExpediaGroupResponseParsingException If the authentication response cannot be parsed
+ * @throws ExpediaGroupNetworkException If a network error occurs during authentication
+ */
+ @Throws(
+ ExpediaGroupAuthException::class,
+ ExpediaGroupResponseParsingException::class,
+ ExpediaGroupNetworkException::class
+ )
+ fun authenticate()
+
+ /**
+ * Clears any stored authentication state.
+ */
+ fun clearAuthentication()
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/common/Credentials.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/common/Credentials.kt
new file mode 100644
index 00000000..dc690ce0
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/authentication/common/Credentials.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.authentication.common
+
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets.ISO_8859_1
+import java.util.Base64
+
+/**
+ * Represents a set of credentials consisting of a key and a secret.
+ *
+ * The `Credentials` class encapsulates authentication details required for accessing secure resources.
+ * It provides functionality to encode the credentials into a `Basic` authentication header value.
+ *
+ * @param key The client key or username for authentication.
+ * @param secret The client secret or password for authentication.
+ */
+data class Credentials(
+ private val key: String,
+ private val secret: String
+) {
+ /**
+ * Encodes the credentials into a `Basic` authentication header value.
+ *
+ * This method combines the `key` and `secret` into a single string in the format `key:secret`,
+ * encodes it using Base64, and prefixes it with `Basic`. The resulting string is suitable for use
+ * in the `Authorization` header of HTTP requests.
+ *
+ * @param charset The character set to use for encoding the credentials. Defaults to [ISO_8859_1].
+ * @return The `Basic` authentication header value as a string.
+ */
+ fun encodeBasic(charset: Charset = ISO_8859_1): String {
+ val keyAndSecret = "$key:$secret"
+ val bytes = keyAndSecret.toByteArray(charset)
+ val encoded = Base64.getEncoder().encodeToString(bytes)
+ return "Basic $encoded"
+ }
+
+ override fun toString(): String = "Credentials(key=$key)"
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/Client.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/Client.kt
deleted file mode 100644
index f7af278b..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/client/Client.kt
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (C) 2022 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.client
-
-import com.expediagroup.sdk.core.configuration.Credentials
-import com.expediagroup.sdk.core.configuration.provider.ConfigurationProvider
-import com.expediagroup.sdk.core.constant.ConfigurationName
-import com.expediagroup.sdk.core.constant.Constant
-import com.expediagroup.sdk.core.constant.provider.ExceptionMessageProvider.getMissingRequiredConfigurationMessage
-import com.expediagroup.sdk.core.constant.provider.LoggingMessageProvider
-import com.expediagroup.sdk.core.contract.Contract
-import com.expediagroup.sdk.core.contract.adhereTo
-import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupConfigurationException
-import com.expediagroup.sdk.core.model.getTransactionId
-import com.expediagroup.sdk.core.plugin.Hooks
-import com.expediagroup.sdk.core.plugin.authentication.AuthenticationConfiguration
-import com.expediagroup.sdk.core.plugin.authentication.AuthenticationHookFactory
-import com.expediagroup.sdk.core.plugin.authentication.AuthenticationPlugin
-import com.expediagroup.sdk.core.plugin.authentication.strategy.AuthenticationStrategy
-import com.expediagroup.sdk.core.plugin.encoding.EncodingConfiguration
-import com.expediagroup.sdk.core.plugin.encoding.EncodingPlugin
-import com.expediagroup.sdk.core.plugin.exception.ExceptionHandlingConfiguration
-import com.expediagroup.sdk.core.plugin.exception.ExceptionHandlingPlugin
-import com.expediagroup.sdk.core.plugin.hooks
-import com.expediagroup.sdk.core.plugin.httptimeout.HttpTimeoutConfiguration
-import com.expediagroup.sdk.core.plugin.httptimeout.HttpTimeoutPlugin
-import com.expediagroup.sdk.core.plugin.logging.ExpediaGroupLoggerFactory
-import com.expediagroup.sdk.core.plugin.logging.LoggingConfiguration
-import com.expediagroup.sdk.core.plugin.logging.LoggingPlugin
-import com.expediagroup.sdk.core.plugin.plugins
-import com.expediagroup.sdk.core.plugin.request.DefaultRequestConfiguration
-import com.expediagroup.sdk.core.plugin.request.DefaultRequestPlugin
-import io.ktor.client.HttpClient
-import io.ktor.client.engine.HttpClientEngine
-import io.ktor.client.engine.okhttp.OkHttp
-import io.ktor.client.statement.HttpResponse
-import io.ktor.client.statement.request
-
-val DEFAULT_HTTP_CLIENT_ENGINE: HttpClientEngine =
- OkHttp.create {
- config {
- eventListener(OkHttpEventListener)
- }
- }
-
-/**
- * The base integration point between the SDK Core and the product SDKs.
- */
-abstract class Client(
- namespace: String,
- environmentProvider: EnvironmentProvider = DefaultEnvironmentProvider(namespace),
-) : EnvironmentProvider by environmentProvider {
- private val httpHandler = DefaultHttpHandler(environmentProvider)
-
- companion object {
- private val log = ExpediaGroupLoggerFactory.getLogger(this::class.java)
- }
-
- /** The configuration provider to use. */
- abstract val configurationProvider: ConfigurationProvider
-
- /** The HTTP client to perform requests with. */
- abstract val httpClient: HttpClient
-
- internal fun buildHttpClient(
- configurationProvider: ConfigurationProvider,
- authenticationType: AuthenticationStrategy.AuthenticationType,
- httpClientEngine: HttpClientEngine = DEFAULT_HTTP_CLIENT_ENGINE,
- ): HttpClient =
- HttpClient(httpClientEngine) {
- val httpClientConfig = this
-
- val key: String = configurationProvider.key ?: fireMissingConfigurationIssue(ConfigurationName.KEY)
- val secret: String = configurationProvider.secret ?: fireMissingConfigurationIssue(ConfigurationName.SECRET)
- val endpoint: String = configurationProvider.endpoint ?: fireMissingConfigurationIssue(ConfigurationName.ENDPOINT)
- val authEndpoint: String = configurationProvider.authEndpoint ?: fireMissingConfigurationIssue(ConfigurationName.AUTH_ENDPOINT)
- val requestTimeout: Long =
- configurationProvider.requestTimeout ?: fireMissingConfigurationIssue(
- ConfigurationName.REQUEST_TIMEOUT_MILLIS,
- )
- val connectionTimeout: Long =
- configurationProvider.connectionTimeout ?: fireMissingConfigurationIssue(
- ConfigurationName.CONNECTION_TIMEOUT_MILLIS,
- )
- val socketTimeout: Long =
- configurationProvider.socketTimeout ?: fireMissingConfigurationIssue(
- ConfigurationName.SOCKET_TIMEOUT_MILLIS,
- )
- val maskedLoggingHeaders: Set = configurationProvider.maskedLoggingHeaders ?: setOf()
- val maskedLoggingBodyFields: Set = configurationProvider.maskedLoggingBodyFields ?: setOf()
-
- val authenticationConfiguration =
- AuthenticationConfiguration.from(
- httpClientConfig,
- Credentials.from(key, secret),
- authEndpoint,
- authenticationType,
- )
-
- plugins {
- use(LoggingPlugin).with(LoggingConfiguration.from(httpClientConfig, maskedLoggingHeaders, maskedLoggingBodyFields))
- use(AuthenticationPlugin).with(authenticationConfiguration)
- use(DefaultRequestPlugin).with(DefaultRequestConfiguration.from(httpClientConfig, endpoint))
- use(EncodingPlugin).with(EncodingConfiguration.from(httpClientConfig))
- use(
- HttpTimeoutPlugin,
- ).with(HttpTimeoutConfiguration.from(httpClientConfig, requestTimeout, connectionTimeout, socketTimeout))
- use(ExceptionHandlingPlugin).with(ExceptionHandlingConfiguration.from(httpClientConfig))
-// use(SerializationPlugin).with(SerializationConfiguration.from(httpClientConfig))
- }
-
- hooks {
- use(AuthenticationHookFactory).with(authenticationConfiguration)
- }
- }
-
- /** Throw an exception if the configuration is missing. */
- private fun fireMissingConfigurationIssue(configurationKey: String): Nothing =
- throw ExpediaGroupConfigurationException(getMissingRequiredConfigurationMessage(configurationKey))
-
- private fun isNotSuccessfulResponse(response: HttpResponse) = response.status.value !in Constant.SUCCESSFUL_STATUS_CODES_RANGE
-
- @Suppress("unused") // This is used by the product SDKs.
- suspend fun throwIfError(response: HttpResponse, operationId: String) {
- if (isNotSuccessfulResponse(response)) {
- log.info(LoggingMessageProvider.getResponseUnsuccessfulMessage(response.status, response.request.headers.getTransactionId()))
- throwServiceException(response, operationId)
- }
- }
-
- abstract suspend fun throwServiceException(
- response: HttpResponse,
- operationId: String,
- )
-
- suspend fun performGet(url: String): HttpResponse = httpHandler.performGet(httpClient, url)
-
- /**
- * A [Client] builder.
- */
- abstract class Builder> {
- /** Sets the API key to use for authentication. */
- protected var key: String? = null
-
- /** Sets the API secret to use for authentication. */
- protected var secret: String? = null
-
- /** Sets the API endpoint to use for requests. */
- protected var endpoint: String? = null
-
- /**
- * Sets the request timeout in milliseconds.
- *
- * Request timeout is the time period from the start of the request to the completion of the response.
- *
- * Default is infinite - no timeout.
- */
- protected var requestTimeout: Long? = null
-
- /**
- * Sets the connection timeout in milliseconds.
- *
- * Connection timeout is the time period from the start of the request to the establishment of the connection with the server.
- *
- * Default is 10 seconds (10000 milliseconds).
- */
- protected var connectionTimeout: Long? = null
-
- /**
- * Sets the socket timeout in milliseconds.
- *
- * Socket timeout is the maximum period of inactivity between two consecutive data packets.
- *
- * Default is 15 seconds (15000 milliseconds).
- */
- protected var socketTimeout: Long? = null
-
- /** Sets tne body fields to be masked in logging. */
- protected var maskedLoggingHeaders: Set? = null
-
- /** Sets tne body fields to be masked in logging. */
- protected var maskedLoggingBodyFields: Set? = null
-
- /** Sets the API key to use for authentication.
- *
- * @param key The API key to use for authentication.
- * @return The [Builder] instance.
- */
- fun key(key: String): SELF {
- this.key = key
- return self()
- }
-
- /** Sets the API secret to use for authentication.
- *
- * @param secret The API secret to use for authentication.
- * @return The [Builder] instance.
- */
- fun secret(secret: String): SELF {
- this.secret = secret
- return self()
- }
-
- /** Sets the API endpoint to use for requests.
- *
- * @param endpoint The API endpoint to use for requests.
- * @return The [Builder] instance.
- */
- fun endpoint(endpoint: String): SELF {
- this.endpoint = endpoint.adhereTo(Contract.TRAILING_SLASH)
- log.info(LoggingMessageProvider.getRuntimeConfigurationProviderMessage(ConfigurationName.ENDPOINT, endpoint))
- return self()
- }
-
- /**
- * Sets the request timeout in milliseconds.
- * Request timeout is the time period from the start of the request to the completion of the response.
- * Default is infinite - no timeout.
- *
- * @param milliseconds The request timeout to be used.
- * @return The [Builder] instance.
- */
- fun requestTimeout(milliseconds: Long): SELF {
- this.requestTimeout = milliseconds
- log.info(
- LoggingMessageProvider.getRuntimeConfigurationProviderMessage(
- ConfigurationName.REQUEST_TIMEOUT_MILLIS,
- milliseconds.toString(),
- ),
- )
- return self()
- }
-
- /**
- * Sets the connection timeout in milliseconds.
- * Connection timeout is the time period from the start of the request to the establishment of the connection with the server.
- * Default is 10 seconds (10000 milliseconds).
- *
- * @param milliseconds The connection timeout to be used.
- * @return The [Builder] instance.
- */
- fun connectionTimeout(milliseconds: Long): SELF {
- this.connectionTimeout = milliseconds
- log.info(
- LoggingMessageProvider.getRuntimeConfigurationProviderMessage(
- ConfigurationName.CONNECTION_TIMEOUT_MILLIS,
- milliseconds.toString(),
- ),
- )
- return self()
- }
-
- /**
- * Sets the socket timeout in milliseconds.
- * Socket timeout is the maximum period of inactivity between two consecutive data packets.
- * Default is 15 seconds (15000 milliseconds).
- *
- * @param milliseconds The socket timeout to be used.
- * @return The [Builder] instance.
- */
- fun socketTimeout(milliseconds: Long): SELF {
- this.socketTimeout = milliseconds
- log.info(
- LoggingMessageProvider.getRuntimeConfigurationProviderMessage(
- ConfigurationName.SOCKET_TIMEOUT_MILLIS,
- milliseconds.toString(),
- ),
- )
- return self()
- }
-
- /**
- * Sets tne headers to be masked in logging.
- *
- * @param headers the headers to be masked in logging.
- * @return The [Builder] instance.
- */
- fun maskedLoggingHeaders(vararg headers: String): SELF {
- this.maskedLoggingHeaders = headers.toSet()
- log.info(
- LoggingMessageProvider.getRuntimeConfigurationProviderMessage(
- ConfigurationName.MASKED_LOGGING_HEADERS,
- headers.joinToString(),
- ),
- )
- return self()
- }
-
- /**
- * Sets tne body fields to be masked in logging.
- *
- * @param fields the body fields to be masked in logging.
- * @return The [Builder] instance.
- */
- fun maskedLoggingBodyFields(vararg fields: String): SELF {
- this.maskedLoggingBodyFields = fields.toSet()
- log.info(
- LoggingMessageProvider.getRuntimeConfigurationProviderMessage(
- ConfigurationName.MASKED_LOGGING_BODY_FIELDS,
- fields.joinToString(),
- ),
- )
- return self()
- }
-
- /** Create a [Client] object. */
- abstract fun build(): Client
-
- @Suppress("UNCHECKED_CAST") // This is safe because of the type parameter
- protected open fun self(): SELF = this as SELF
- }
-}
-
-/** Executes the hooks for the client. */
-fun T.finalize() = Hooks.execute(this)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/ClientHelpers.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/ClientHelpers.kt
deleted file mode 100644
index b2ed3e34..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/client/ClientHelpers.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2022 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.client
-
-/** Handy utils and helpers for a client. */
-abstract class ClientHelpers(
- val client: Client
-)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/Environment.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/Environment.kt
deleted file mode 100644
index 4b134f4a..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/client/Environment.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.expediagroup.sdk.core.client
-
-import com.expediagroup.sdk.core.constant.HeaderKey
-import com.expediagroup.sdk.core.model.Properties
-import io.ktor.client.request.HttpRequestBuilder
-import io.ktor.http.HttpHeaders
-import java.util.UUID
-
-interface EnvironmentProvider {
- fun HttpRequestBuilder.appendHeaders()
-}
-
-class DefaultEnvironmentProvider(
- namespace: String
-) : EnvironmentProvider {
- private var properties: Map
-
- init{
- val propertiesResource = javaClass.classLoader.getResource("sdk.properties")
- properties = emptyMap()
- }
-
- private val javaVersion = System.getProperty("java.version")
- private val operatingSystemName = System.getProperty("os.name")
- private val operatingSystemVersion = System.getProperty("os.version")
-// private val userAgent = "expediagroup-sdk-java-$namespace/${properties["sdk-version"]!!} (Java $javaVersion; $operatingSystemName $operatingSystemVersion)"
-
- override fun HttpRequestBuilder.appendHeaders() {
- with(headers) {
- append(HttpHeaders.UserAgent, "")
- append(HeaderKey.X_SDK_TITLE, "")
- append(HeaderKey.TRANSACTION_ID, UUID.randomUUID().toString())
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/ExpediaGroupClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/ExpediaGroupClient.kt
deleted file mode 100644
index cf28f895..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/client/ExpediaGroupClient.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2022 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.client
-
-import com.expediagroup.sdk.core.configuration.ExpediaGroupClientConfiguration
-import com.expediagroup.sdk.core.configuration.collector.ConfigurationCollector
-import com.expediagroup.sdk.core.configuration.provider.ConfigurationProvider
-import com.expediagroup.sdk.core.configuration.provider.ExpediaGroupConfigurationProvider
-import com.expediagroup.sdk.core.plugin.authentication.strategy.AuthenticationStrategy
-import io.ktor.client.HttpClient
-import io.ktor.client.engine.HttpClientEngine
-
-/**
- * The integration point between the SDK Core and the product SDKs.
- *
- * @param httpClientEngine The HTTP client engine to use.
- * @param clientConfiguration The configuration for the client.
- */
-abstract class ExpediaGroupClient(
- namespace: String,
- clientConfiguration: ExpediaGroupClientConfiguration,
- httpClientEngine: HttpClientEngine = DEFAULT_HTTP_CLIENT_ENGINE
-) : Client(namespace) {
- private val _configurationProvider: ConfigurationProvider =
- ConfigurationCollector.create(
- clientConfiguration.toProvider(),
- ExpediaGroupConfigurationProvider
- )
- private val _httpClient: HttpClient = buildHttpClient(_configurationProvider, AuthenticationStrategy.AuthenticationType.BEARER, httpClientEngine)
-
- init {
- finalize()
- }
-
- override val configurationProvider: ConfigurationProvider
- get() = _configurationProvider
-
- override val httpClient: HttpClient
- get() = _httpClient
-
- /** An [ExpediaGroupClient] builder. */
- @Suppress("unused") // This is used by the generated SDK clients.
- abstract class Builder> : Client.Builder() {
- /** Sets the API auth endpoint to use for requests. */
- protected var authEndpoint: String? = null
-
- /** Sets the API auth endpoint to use for requests.
- *
- * @param authEndpoint The API auth endpoint to use for requests.
- * @return The [Builder] instance.
- */
- fun authEndpoint(authEndpoint: String): SELF {
- this.authEndpoint = authEndpoint
- return self()
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/HttpHandler.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/HttpHandler.kt
deleted file mode 100644
index 548fad03..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/client/HttpHandler.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2022 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.client
-
-import io.ktor.client.HttpClient
-import io.ktor.client.request.request
-import io.ktor.client.request.url
-import io.ktor.client.statement.HttpResponse
-import io.ktor.http.HttpMethod
-
-internal interface HttpHandler {
- suspend fun performGet(
- httpClient: HttpClient,
- link: String
- ): HttpResponse
-}
-
-internal class DefaultHttpHandler(
- private val environmentProvider: EnvironmentProvider
-) : HttpHandler, EnvironmentProvider by environmentProvider {
- override suspend fun performGet(
- httpClient: HttpClient,
- link: String
- ): HttpResponse {
- return httpClient.request {
- method = HttpMethod.Get
- url(link)
- appendHeaders()
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/OkHttpEventListener.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/OkHttpEventListener.kt
deleted file mode 100644
index 25dbd741..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/client/OkHttpEventListener.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2022 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.client
-
-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 java.io.IOException
-import java.net.InetSocketAddress
-import java.net.Proxy
-
-object OkHttpEventListener : EventListener() {
- private val log = ExpediaGroupLoggerFactory.getLogger(this::class.java)
-
- fun Call.getTransactionId() = request().headers[HeaderKey.TRANSACTION_ID]
-
- override fun callStart(call: Call) {
- super.callStart(call)
- log.debug("Call start for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun callEnd(call: Call) {
- super.callEnd(call)
- log.debug("Call end for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun callFailed(
- call: Call,
- ioe: IOException
- ) {
- super.callFailed(call, ioe)
- log.debug("Call failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}")
- }
-
- override fun canceled(call: Call) {
- super.canceled(call)
- log.debug("Call canceled for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun connectStart(
- call: Call,
- inetSocketAddress: InetSocketAddress,
- proxy: Proxy
- ) {
- super.connectStart(call, inetSocketAddress, proxy)
- log.debug("Connect start for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun connectEnd(
- call: Call,
- inetSocketAddress: InetSocketAddress,
- proxy: Proxy,
- protocol: Protocol?
- ) {
- super.connectEnd(call, inetSocketAddress, proxy, protocol)
- log.debug("Connect end for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun connectFailed(
- call: Call,
- inetSocketAddress: InetSocketAddress,
- proxy: Proxy,
- protocol: Protocol?,
- ioe: IOException
- ) {
- super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe)
- log.debug("Connect failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}")
- }
-
- override fun connectionAcquired(
- call: Call,
- connection: Connection
- ) {
- super.connectionAcquired(call, connection)
- log.debug("Connection acquired for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun connectionReleased(
- call: Call,
- connection: Connection
- ) {
- super.connectionReleased(call, connection)
- log.debug("Connection released for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun secureConnectStart(call: Call) {
- super.secureConnectStart(call)
- log.debug("Secure connect start for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun secureConnectEnd(
- call: Call,
- handshake: Handshake?
- ) {
- super.secureConnectEnd(call, handshake)
- log.debug("Secure connect end for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun requestHeadersStart(call: Call) {
- super.requestHeadersStart(call)
- log.debug("Sending request headers start for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun requestHeadersEnd(
- call: Call,
- request: Request
- ) {
- super.requestHeadersEnd(call, request)
- log.debug("Sending request headers end for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun requestBodyStart(call: Call) {
- super.requestBodyStart(call)
- log.debug("Sending request body start for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun requestBodyEnd(
- call: Call,
- byteCount: Long
- ) {
- super.requestBodyEnd(call, byteCount)
- log.debug("Sending request body end for transaction-id: [${call.getTransactionId()}] with byte count: $byteCount")
- }
-
- override fun requestFailed(
- call: Call,
- ioe: IOException
- ) {
- super.requestFailed(call, ioe)
- log.debug("Request failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}")
- }
-
- override fun responseHeadersStart(call: Call) {
- super.responseHeadersStart(call)
- log.debug("Receiving response headers start for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun responseHeadersEnd(
- call: Call,
- response: Response
- ) {
- super.responseHeadersEnd(call, response)
- log.debug("Receiving response headers end for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun responseBodyStart(call: Call) {
- super.responseBodyStart(call)
- log.debug("Receiving response body start for transaction-id: [${call.getTransactionId()}]")
- }
-
- override fun responseBodyEnd(
- call: Call,
- byteCount: Long
- ) {
- super.responseBodyEnd(call, byteCount)
- log.debug("Receiving response body end for transaction-id: [${call.getTransactionId()}] with byte count: $byteCount")
- }
-
- override fun responseFailed(
- call: Call,
- ioe: IOException
- ) {
- super.responseFailed(call, ioe)
- log.debug("Receiving response failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}")
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/RequestExecutor.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/RequestExecutor.kt
new file mode 100644
index 00000000..39c5b477
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/client/RequestExecutor.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.client
+
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.Response
+import com.expediagroup.sdk.core.interceptor.Interceptor
+import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupNetworkException
+
+/**
+ * Abstract base class for processing HTTP requests within the SDK.
+ *
+ * This class serves as the main entry point for executing HTTP requests through the SDK.
+ * It enhances the basic [Transport] functionality by:
+ *
+ * 1. Applying request/response interceptors
+ * 2. Enforcing SDK-specific policies and rules (e.g. authentication)
+ * 3. Providing common error handling and retry logic (if needed)
+ * 4. Managing request/response lifecycle and transformation
+ *
+ * Implementations should:
+ * - Define the order and types of interceptors to be applied
+ * - Implement any SDK-specific error handling or retry logic
+ * - Handle request/response transformation and validation
+ *
+ * Example implementation:
+ * ```
+ * class SdkRequestProcessor(transport: Transport) : RequestProcessor(transport) {
+ * override val interceptors = listOf(
+ * AuthenticationInterceptor(),
+ * LoggingInterceptor(),
+ * RetryInterceptor()
+ * )
+ *
+ * override fun execute(request: Request) = executeWithInterceptors(request)
+ * }
+ * ```
+ *
+ * @param transport The transport implementation to use for executing requests
+ */
+abstract class RequestExecutor(protected val transport: Transport) {
+ /**
+ * List of interceptors to be applied to requests in order.
+ *
+ * Interceptors can modify requests before they are sent and responses
+ * before they are returned to the caller. Common use cases include:
+ * - Adding authentication headers
+ * - Logging
+ * - Retry logic
+ * - Request/response validation
+ * - Error handling
+ */
+ protected abstract val interceptors: List
+
+ /**
+ * Executes an HTTP request synchronously, applying all configured interceptors.
+ *
+ * @param request The request to execute
+ * @return The response from the server after passing through interceptors
+ * @throws ExpediaGroupNetworkException If any network-related error occurs
+ */
+ @Throws(ExpediaGroupNetworkException::class)
+ abstract fun execute(request: Request): Response
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/client/Transport.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/client/Transport.kt
new file mode 100644
index 00000000..4de0446c
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/client/Transport.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.client
+
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.Response
+import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupNetworkException
+
+/**
+ * A transport layer interface that adapts different HTTP client libraries to work with the SDK.
+ *
+ * This interface serves as an abstraction layer between the SDK and the underlying HTTP client,
+ * allowing users to integrate their preferred HTTP client library while maintaining consistent
+ * behavior across the SDK. Implementers are responsible for:
+ *
+ * 1. Converting SDK request/response models to their HTTP client's models
+ * 2. Handling HTTP client-specific configuration and setup
+ * 3. Managing resources and connections appropriately
+ *
+ * Example implementation using OkHttp:
+ * ```
+ * class OkHttpTransport(private val client: OkHttpClient) : Transport {
+ * override fun execute(request: Request): Response {
+ * val okHttpRequest = request.toOkHttpRequest()
+ * return client.newCall(okHttpRequest).execute().toSdkResponse()
+ * }
+ * }
+ * ```
+ *
+ * @see Request SDK request model that wraps HTTP request details
+ * @see Response SDK response model that wraps HTTP response details
+ */
+interface Transport {
+ /**
+ * Executes an HTTP request synchronously.
+ *
+ * This method should:
+ * - Convert the SDK request to the HTTP client's request format
+ * - Execute the request using the underlying HTTP client
+ * - Convert the HTTP client's response to the SDK response format
+ * - Handle errors appropriately by throwing [ExpediaGroupNetworkException]
+ *
+ * @param request The SDK request to execute
+ * @return The response from the server wrapped in the SDK response model
+ * @throws ExpediaGroupNetworkException If any network-related error occurs during execution
+ */
+ @Throws(ExpediaGroupNetworkException::class)
+ fun execute(request: Request): Response
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/ClientConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/ClientConfiguration.kt
deleted file mode 100644
index d8dfdedd..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/ClientConfiguration.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration
-
-import com.expediagroup.sdk.core.configuration.provider.RuntimeConfigurationProvider
-
-interface ClientConfiguration {
- val key: String?
- val secret: String?
- val endpoint: String?
- val requestTimeout: Long?
- val connectionTimeout: Long?
- val socketTimeout: Long?
- val maskedLoggingHeaders: Set?
- val maskedLoggingBodyFields: Set?
-
- /** Build a [RuntimeConfigurationProvider] from a [ClientConfiguration]. */
- fun toProvider(): RuntimeConfigurationProvider =
- RuntimeConfigurationProvider(
- key = key,
- secret = secret,
- endpoint = endpoint,
- requestTimeout = requestTimeout,
- connectionTimeout = connectionTimeout,
- socketTimeout = socketTimeout,
- maskedLoggingHeaders = maskedLoggingHeaders,
- maskedLoggingBodyFields = maskedLoggingBodyFields
- )
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/Credentials.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/Credentials.kt
deleted file mode 100644
index 1e249b81..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/Credentials.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration
-
-/**
- * A pair of key-secret.
- *
- * @property key the client key
- * @property secret the client secret
- */
-internal data class Credentials(
- val key: String,
- val secret: String
-) {
- /**
- * A factory of [Credentials].
- */
- companion object Factory {
- /**
- * Create a [Credentials] object.
- *
- * @param key Client key.
- * @param secret Client secret.
- * @return ClientCredentials object.
- */
- @JvmStatic
- fun from(
- key: String,
- secret: String
- ): Credentials = Credentials(key, secret)
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/ExpediaGroupClientConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/ExpediaGroupClientConfiguration.kt
deleted file mode 100644
index e5289d9e..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/ExpediaGroupClientConfiguration.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration
-
-import com.expediagroup.sdk.core.client.ExpediaGroupClient
-import com.expediagroup.sdk.core.configuration.provider.RuntimeConfigurationProvider
-
-/**
- * Configuration for the [ExpediaGroupClient].
- *
- * @property key The API key to use for authentication.
- * @property secret The API secret to use for authentication.
- * @property endpoint The API endpoint to use for requests.
- * @property requestTimeout The request timeout to be used in milliseconds.
- * @property connectionTimeout The connection timeout to be used in milliseconds.
- * @property socketTimeout The socket timeout to be used in milliseconds.
- * @property maskedLoggingHeaders The headers to be masked in logging.
- * @property maskedLoggingBodyFields The body fields to be masked in logging.
- * @property authEndpoint The API endpoint to use for authentication.
- */
-data class ExpediaGroupClientConfiguration(
- override val key: String? = null,
- override val secret: String? = null,
- override val endpoint: String? = null,
- override val requestTimeout: Long? = null,
- override val connectionTimeout: Long? = null,
- override val socketTimeout: Long? = null,
- override val maskedLoggingHeaders: Set? = null,
- override val maskedLoggingBodyFields: Set? = null,
- val authEndpoint: String? = null
-) : ClientConfiguration {
- /** Build a [RuntimeConfigurationProvider] from an [ExpediaGroupClientConfiguration]. */
- override fun toProvider(): RuntimeConfigurationProvider = super.toProvider().copy(authEndpoint = authEndpoint)
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/collector/ConfigurationCollector.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/collector/ConfigurationCollector.kt
deleted file mode 100644
index a83d40fb..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/collector/ConfigurationCollector.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration.collector
-
-import com.expediagroup.sdk.core.configuration.provider.ConfigurationProvider
-import com.expediagroup.sdk.core.constant.ConfigurationName.AUTH_ENDPOINT
-import com.expediagroup.sdk.core.constant.ConfigurationName.CONFIGURATION_COLLECTOR
-import com.expediagroup.sdk.core.constant.ConfigurationName.CONNECTION_TIMEOUT_MILLIS
-import com.expediagroup.sdk.core.constant.ConfigurationName.ENDPOINT
-import com.expediagroup.sdk.core.constant.ConfigurationName.KEY
-import com.expediagroup.sdk.core.constant.ConfigurationName.MASKED_LOGGING_BODY_FIELDS
-import com.expediagroup.sdk.core.constant.ConfigurationName.MASKED_LOGGING_HEADERS
-import com.expediagroup.sdk.core.constant.ConfigurationName.REQUEST_TIMEOUT_MILLIS
-import com.expediagroup.sdk.core.constant.ConfigurationName.SECRET
-import com.expediagroup.sdk.core.constant.ConfigurationName.SOCKET_TIMEOUT_MILLIS
-import com.expediagroup.sdk.core.constant.provider.LoggingMessageProvider
-import com.expediagroup.sdk.core.plugin.logging.ExpediaGroupLoggerFactory
-
-/**
- * Configuration collector that collects configuration from all available providers.
- *
- * @param providers A configuration providers queue.
- */
-internal class ConfigurationCollector private constructor(providers: ConfigurationProviderQueue) : ConfigurationProvider {
- override val name: String = CONFIGURATION_COLLECTOR
-
- companion object Factory {
- private val log = ExpediaGroupLoggerFactory.getLogger(ConfigurationCollector::class.java)
-
- /**
- * Creates a new [ConfigurationCollector] with the given [providerQueue].
- *
- * @param providerQueue the [ConfigurationProviderQueue] to use.
- * @return a new [ConfigurationCollector] with the given [providerQueue].
- */
- fun create(providerQueue: ConfigurationProviderQueue): ConfigurationCollector = ConfigurationCollector(providerQueue)
-
- /**
- * Creates a new [ConfigurationCollector] with the given [providers].
- *
- * @param providers the [ConfigurationProvider]s to use.
- * @return a new [ConfigurationCollector] with the given [providers].
- */
- fun create(vararg providers: ConfigurationProvider): ConfigurationCollector = create(ConfigurationProviderQueue.from(providers.asList()))
- }
-
- override val key: String? = providers.firstWith { it.key }.also { it?.log(KEY) }?.retrieve()
- override val secret: String? = providers.firstWith { it.secret }.also { it?.log(SECRET) }?.retrieve()
- override val endpoint: String? = providers.firstWith { it.endpoint }.also { it?.log(ENDPOINT) }?.retrieve()
- override val authEndpoint: String? = providers.firstWith { it.authEndpoint }.also { it?.log(AUTH_ENDPOINT) }?.retrieve()
- override val requestTimeout: Long? = providers.firstWith { it.requestTimeout }.also { it?.log(REQUEST_TIMEOUT_MILLIS) }?.retrieve()
- override val connectionTimeout: Long? = providers.firstWith { it.connectionTimeout }.also { it?.log(CONNECTION_TIMEOUT_MILLIS) }?.retrieve()
- override val socketTimeout: Long? = providers.firstWith { it.socketTimeout }.also { it?.log(SOCKET_TIMEOUT_MILLIS) }?.retrieve()
- override val maskedLoggingHeaders: Set? = providers.firstWith { it.maskedLoggingHeaders }.also { it?.log(MASKED_LOGGING_HEADERS) }?.retrieve()
- override val maskedLoggingBodyFields: Set? = providers.firstWith { it.maskedLoggingBodyFields }.also { it?.log(MASKED_LOGGING_BODY_FIELDS) }?.retrieve()
-
- private fun ProvidedConfiguration.log(configurationName: String) {
- log.info(LoggingMessageProvider.getChosenProviderMessage(configurationName, providerName))
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/collector/ConfigurationProviderQueue.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/collector/ConfigurationProviderQueue.kt
deleted file mode 100644
index 3a964739..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/collector/ConfigurationProviderQueue.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration.collector
-
-import com.expediagroup.sdk.core.configuration.provider.ConfigurationProvider
-
-/**
- * A queue of all configuration providers.
- *
- * @property providers List of configuration providers
- */
-internal class ConfigurationProviderQueue private constructor(private val providers: List) {
- /** Returns the first provider in the queue. */
- fun first(): ConfigurationProvider? = providers.firstOrNull()
-
- /** Returns the first provider in the queue that matches the given [predicate]. */
- fun first(predicate: (ConfigurationProvider) -> Boolean): ConfigurationProvider? = providers.firstOrNull(predicate)
-
- /** Returns the first provider in the queue that matches the given [predicate] if found, null otherwise.*/
- fun firstWith(predicate: (provider: ConfigurationProvider) -> T?): ProvidedConfiguration? = first { predicate(it) != null }?.let { ProvidedConfiguration(predicate(it)!!, it.name) }
-
- companion object {
- /** Builds a [ConfigurationProviderQueue] from the given [providers].
- *
- * @param providers the providers to build the queue from.
- * @return a [ConfigurationProviderQueue] instance.
- */
- fun from(providers: List) = ConfigurationProviderQueue(providers.toList())
- }
-}
-
-internal data class ProvidedConfiguration(private val configuration: T, val providerName: String) {
- fun retrieve(): T = configuration
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/ConfigurationProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/ConfigurationProvider.kt
deleted file mode 100644
index ac7f7ea4..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/ConfigurationProvider.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration.provider
-
-import com.expediagroup.sdk.core.constant.Constant
-
-/**
- * A configuration provider that can be used to provide configuration values.
- */
-interface ConfigurationProvider {
- /** The name of the provider. */
- val name: String
-
- /** The API key to use for authentication. */
- val key: String?
- get() = Constant.EMPTY_STRING
-
- /** The API secret to use for authentication. */
- val secret: String?
- get() = Constant.EMPTY_STRING
-
- /** The API endpoint to use for requests. */
- val endpoint: String?
-
- /** The API endpoint to use for authentication. */
- val authEndpoint: String?
- get() = Constant.EMPTY_STRING
-
- /** The time period from the start of the request to the completion of the response. */
- val requestTimeout: Long?
-
- /** The time period from the start of the request to the establishment of the connection with the server. */
- val connectionTimeout: Long?
-
- /** The maximum period of inactivity between two consecutive data packets. */
- val socketTimeout: Long?
-
- /** The headers to be masked in logging. */
- val maskedLoggingHeaders: Set?
- get() = setOf()
-
- /** The body fields to be masked in logging.*/
- val maskedLoggingBodyFields: Set?
- get() = setOf()
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/ExpediaGroupConfigurationProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/ExpediaGroupConfigurationProvider.kt
deleted file mode 100644
index 75295aeb..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/ExpediaGroupConfigurationProvider.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration.provider
-
-import com.expediagroup.sdk.core.configuration.provider.ExpediaGroupConfigurationProvider.authEndpoint
-import com.expediagroup.sdk.core.configuration.provider.ExpediaGroupConfigurationProvider.connectionTimeout
-import com.expediagroup.sdk.core.configuration.provider.ExpediaGroupConfigurationProvider.endpoint
-import com.expediagroup.sdk.core.configuration.provider.ExpediaGroupConfigurationProvider.name
-import com.expediagroup.sdk.core.configuration.provider.ExpediaGroupConfigurationProvider.requestTimeout
-import com.expediagroup.sdk.core.configuration.provider.ExpediaGroupConfigurationProvider.socketTimeout
-import com.expediagroup.sdk.core.constant.Constant
-
-/**
- * Default configuration provider for ExpediaGroup.
- *
- * @property name The name of the provider.
- * @property endpoint The API endpoint to use for requests.
- * @property authEndpoint The API endpoint to use for authentication.
- * @property requestTimeout The API response timeout to use for requests.
- * @property connectionTimeout The connection timeout to be used in milliseconds.
- * @property socketTimeout The socket timeout to be used in milliseconds.
- */
-internal object ExpediaGroupConfigurationProvider : ConfigurationProvider {
- override val name: String = "ExpediaGroup Configuration Provider"
- override val endpoint: String = "https://api.expediagroup.com/"
- override val authEndpoint: String = "${endpoint}identity/oauth2/v3/token/"
- override val requestTimeout: Long = Constant.INFINITE_TIMEOUT
- override val connectionTimeout: Long = Constant.TEN_SECONDS_IN_MILLIS
- override val socketTimeout: Long = Constant.FIFTEEN_SECONDS_IN_MILLIS
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/RapidConfigurationProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/RapidConfigurationProvider.kt
deleted file mode 100644
index bc8e06c6..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/RapidConfigurationProvider.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration.provider
-
-import com.expediagroup.sdk.core.configuration.provider.RapidConfigurationProvider.connectionTimeout
-import com.expediagroup.sdk.core.configuration.provider.RapidConfigurationProvider.endpoint
-import com.expediagroup.sdk.core.configuration.provider.RapidConfigurationProvider.name
-import com.expediagroup.sdk.core.configuration.provider.RapidConfigurationProvider.requestTimeout
-import com.expediagroup.sdk.core.configuration.provider.RapidConfigurationProvider.socketTimeout
-import com.expediagroup.sdk.core.constant.Constant
-
-/**
- * Default configuration provider for Rapid.
- *
- * @property name The name of the provider.
- * @property endpoint The API endpoint to use for requests.
- * @property requestTimeout The API response timeout to use for requests.
- * @property connectionTimeout The connection timeout to use for requests.
- * @property socketTimeout The socket timeout to use for requests.
- */
-internal object RapidConfigurationProvider : ConfigurationProvider {
- override val name: String = "Rapid Configuration Provider"
- override val endpoint: String = "https://api.ean.com/v3"
- override val requestTimeout: Long = Constant.INFINITE_TIMEOUT
- override val connectionTimeout: Long = Constant.TEN_SECONDS_IN_MILLIS
- override val socketTimeout: Long = Constant.FIFTEEN_SECONDS_IN_MILLIS
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/RuntimeConfigurationProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/RuntimeConfigurationProvider.kt
deleted file mode 100644
index fd72dc58..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/configuration/provider/RuntimeConfigurationProvider.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2022 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.configuration.provider
-
-import com.expediagroup.sdk.core.constant.ConfigurationName.RUNTIME_CONFIGURATION_PROVIDER
-
-/**
- * A runtime-built configuration provider.
- *
- * @property name The name of the provider.
- * @property key The API key to use for authentication.
- * @property secret The API secret to use for authentication.
- * @property endpoint The API endpoint to use for requests.
- * @property authEndpoint The API endpoint to use for authentication.
- * @property requestTimeout The request timeout to be used in milliseconds.
- * @property connectionTimeout The connection timeout to be used in milliseconds.
- * @property socketTimeout The socket timeout to be used in milliseconds.
- * @property maskedLoggingHeaders The headers to be masked in logging.
- * @property maskedLoggingBodyFields The body fields to be masked in logging.
- */
-data class RuntimeConfigurationProvider(
- override val name: String = RUNTIME_CONFIGURATION_PROVIDER,
- override val key: String? = null,
- override val secret: String? = null,
- override val endpoint: String? = null,
- override val authEndpoint: String? = null,
- override val requestTimeout: Long? = null,
- override val connectionTimeout: Long? = null,
- override val socketTimeout: Long? = null,
- override val maskedLoggingHeaders: Set? = null,
- override val maskedLoggingBodyFields: Set? = null
-) : ConfigurationProvider
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Authentication.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Authentication.kt
deleted file mode 100644
index 2b22a50e..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Authentication.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object Authentication {
- const val AUTHORIZATION_REQUEST_LOCK_DELAY = 20L
- const val BEARER_EXPIRY_DATE_MARGIN: Long = 10 // In seconds
-
- const val EAN = "EAN"
-
- const val BEARER = "Bearer"
-
- const val GRANT_TYPE = "grant_type"
-
- const val CLIENT_CREDENTIALS = "client_credentials"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/ConfigurationName.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/ConfigurationName.kt
deleted file mode 100644
index 0004086b..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/ConfigurationName.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object ConfigurationName {
- const val KEY = "key"
-
- const val SECRET = "secret"
-
- const val ENDPOINT = "endpoint"
-
- const val AUTH_ENDPOINT = "auth endpoint"
-
- const val REQUEST_TIMEOUT_MILLIS = "request timeout in milliseconds"
-
- const val CONNECTION_TIMEOUT_MILLIS = "connection timeout in milliseconds"
-
- const val SOCKET_TIMEOUT_MILLIS = "socket timeout in milliseconds"
-
- const val MASKED_LOGGING_HEADERS = "masked logging headers"
-
- const val MASKED_LOGGING_BODY_FIELDS = "masked logging body fields"
-
- const val RUNTIME_CONFIGURATION_PROVIDER = "runtime configuration"
-
- const val CONFIGURATION_COLLECTOR = "configuration collector"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Constant.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Constant.kt
deleted file mode 100644
index 5f7a9d8c..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Constant.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-import io.ktor.client.plugins.HttpTimeout
-
-internal object Constant {
- const val EMPTY_STRING = ""
- const val TEN_SECONDS_IN_MILLIS = 10_000L
- const val FIFTEEN_SECONDS_IN_MILLIS = 15_000L
- const val INFINITE_TIMEOUT = HttpTimeout.INFINITE_TIMEOUT_MS
-
- private const val SUCCESSFUL_STATUS_CODES_RANGE_START = 200
- private const val SUCCESSFUL_STATUS_CODES_RANGE_END = 299
- val SUCCESSFUL_STATUS_CODES_RANGE: IntRange = SUCCESSFUL_STATUS_CODES_RANGE_START..SUCCESSFUL_STATUS_CODES_RANGE_END
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/ExceptionMessage.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/ExceptionMessage.kt
deleted file mode 100644
index 33220440..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/ExceptionMessage.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object ExceptionMessage {
- const val AUTHENTICATION_FAILURE = "Unable to authenticate"
-
- const val AUTHENTICATION_NOT_CONFIGURED_FOR_CLIENT = "Authentication is not configured"
-
- const val LOGGING_MASKED_FIELDS_NOT_CONFIGURED_FOR_CLIENT = "Logging masked fields is not configured"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/HeaderKey.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/HeaderKey.kt
deleted file mode 100644
index d3a72279..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/HeaderKey.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object HeaderKey {
- const val PAGINATION_TOTAL_RESULTS = "pagination-total-results"
-
- const val LINK = "link"
-
- const val TRANSACTION_ID = "transaction-id"
-
- const val X_SDK_TITLE = "x-sdk-title"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/HeaderValue.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/HeaderValue.kt
deleted file mode 100644
index 3c418bc7..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/HeaderValue.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object HeaderValue {
- const val GZIP = "gzip"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Key.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Key.kt
deleted file mode 100644
index a7a71ca1..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/Key.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object Key {
- const val CLIENT_KEY = "client_key"
-
- const val CLIENT_SECRET = "client_secret"
-
- const val ENDPOINT = "endpoint"
-
- const val AUTH_ENDPOINT = "auth_endpoint"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LogMaskingFields.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LogMaskingFields.kt
deleted file mode 100644
index b2c4a327..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LogMaskingFields.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-import io.ktor.http.HttpHeaders
-
-internal data object LogMaskingFields {
- val DEFAULT_MASKED_HEADER_FIELDS: Set = setOf(HttpHeaders.Authorization)
- val DEFAULT_MASKED_BODY_FIELDS: Set =
- setOf(
- "cvv",
- "pin",
- "card_cvv",
- "card_cvv2",
- "card_number",
- "access_token",
- "security_code",
- "account_number",
- "card_avs_response",
- "card_cvv_response",
- "card_cvv2_response",
- "verificationNumber",
- "vatNumber"
- )
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LogMaskingRegex.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LogMaskingRegex.kt
deleted file mode 100644
index 63d51c0b..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LogMaskingRegex.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object LogMaskingRegex {
- val FIELD_REGEX = "^[a-zA-Z0-9-_]+$".toRegex()
-
- val NUMBER_FIELD_REGEX = "(?<=[\"']?number[\"']?:\\s?[\"'])(\\s*\\d{15,16}\\s*)(?=[\"'])".toRegex()
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LoggerName.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LoggerName.kt
deleted file mode 100644
index 749fcd3f..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LoggerName.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object LoggerName {
- const val REQUEST_BODY_LOGGER: String = "RequestBodyLogger"
- const val RESPONSE_BODY_LOGGER: String = "ResponseBodyLogger"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LoggingMessage.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LoggingMessage.kt
deleted file mode 100644
index 50d9b3bc..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/LoggingMessage.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object LoggingMessage {
- const val LOGGING_PREFIX = "ExpediaSDK:"
-
- const val TOKEN_RENEWAL_IN_PROGRESS = "Renewing token"
-
- const val TOKEN_RENEWAL_SUCCESSFUL = "Token renewal successful"
-
- const val TOKEN_CLEARING_IN_PROGRESS = "Clearing tokens"
-
- const val TOKEN_CLEARING_SUCCESSFUL = "Tokens successfully cleared"
-
- const val TOKEN_EXPIRED = "Token expired or is about to expire. Request will wait until token is renewed"
-
- const val OMITTED = "<-- omitted -->"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/SignatureValues.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/SignatureValues.kt
deleted file mode 100644
index cdf04e37..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/SignatureValues.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant
-
-internal object SignatureValues {
- const val ONE_BYTE_MASK = 0xFF
-
- const val INCREMENT = 0x100
-
- const val RADIX = 16
-
- const val API_KEY = "apikey"
-
- const val SIGNATURE = "signature"
-
- const val TIMESTAMP = "timestamp"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/ExceptionMessageProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/ExceptionMessageProvider.kt
deleted file mode 100644
index 316ef390..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/ExceptionMessageProvider.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant.provider
-
-import com.expediagroup.sdk.core.constant.provider.LoggingMessageProvider.getTransactionIdMessage
-
-internal object ExceptionMessageProvider {
- fun getMissingRequiredConfigurationMessage(name: String): String = "Missing required configuration: $name"
-
- fun getExceptionOccurredWithTransactionIdMessage(
- transactionId: String?,
- message: String?
- ): String = "Exception occurred" + getTransactionIdMessage(transactionId) + getConcatenatedMessage(message)
-
- private fun getConcatenatedMessage(message: String?) = if (message != null) ": $message" else ""
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/LogMaskingRegexProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/LogMaskingRegexProvider.kt
deleted file mode 100644
index 475e6f4c..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/LogMaskingRegexProvider.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant.provider
-
-internal object LogMaskingRegexProvider {
- fun getMaskedFieldsRegex(maskedBodyFields: Set) = "(?<=[\"']?(${maskedBodyFields.joinToString("|")})[\"']?:\\s?[\"'])(\\s*[^\"']+\\s*)(?=[\"'])".toRegex()
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/LoggingMessageProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/LoggingMessageProvider.kt
deleted file mode 100644
index f9d358ad..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/constant/provider/LoggingMessageProvider.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2022 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.constant.provider
-
-import io.ktor.http.HttpStatusCode
-
-internal object LoggingMessageProvider {
- fun getTokenExpiresInMessage(expiresIn: Int) = "New token expires in $expiresIn seconds"
-
- fun getResponseUnsuccessfulMessage(
- httpStatusCode: HttpStatusCode,
- transactionId: String?
- ) = "Unsuccessful response [$httpStatusCode]${getTransactionIdMessage(transactionId)}"
-
- fun getChosenProviderMessage(
- property: String,
- providerName: String
- ) = "Successfully loaded [$property] from [$providerName]"
-
- fun getRuntimeConfigurationProviderMessage(
- property: String,
- value: T
- ) = "Setting [$property] to [$value] from runtime configuration provider"
-
- fun getResponseBodyMessage(
- body: String,
- transactionId: String?
- ) = "Response Body${getTransactionIdMessage(transactionId)}: $body"
-
- fun getRequestBodyMessage(
- body: String,
- transactionId: String?
- ) = "Request Body${getTransactionIdMessage(transactionId)}: $body"
-
- fun getTransactionIdMessage(transactionId: String?) = if (transactionId != null) " for transaction-id [$transactionId]" else ""
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/contract/Contract.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/contract/Contract.kt
deleted file mode 100644
index 6527759c..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/contract/Contract.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2022 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.contract
-
-internal typealias Operation = (String) -> String
-
-/**
- * A contract for a specific [operation].
- *
- * @property operation The operation to perform on a string.
- */
-internal enum class Contract(val operation: Operation) {
- TRAILING_SLASH({ if (it.endsWith("/")) it else "$it/" })
-}
-
-/**
- * Adheres to the given [contract] on a [String].
- *
- * @param contract the [Contract] to adhere to.
- * @return the [String] adhering to the given [contract].
- */
-internal fun String.adhereTo(contract: Contract): String = contract.operation(this)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/extension/NullableExtension.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/extension/NullableExtension.kt
index 855584e0..759b8985 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/extension/NullableExtension.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/extension/NullableExtension.kt
@@ -4,6 +4,6 @@ inline fun T?.getOrThrow(exceptionProvider: () -> Throwable): T {
return this ?: throw exceptionProvider()
}
-fun Boolean?.orFalseIfNull(): Boolean = this ?: false
+fun Boolean?.orFalseIfNull(): Boolean = this == true
fun String?.orNullIfBlank(): String? = this?.takeUnless { it.isBlank() }
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/Headers.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Headers.kt
new file mode 100644
index 00000000..efb030a9
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Headers.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.http
+
+import java.util.Locale
+
+/**
+ * Represents a collection of HTTP headers.
+ */
+class Headers private constructor(private val headersMap: Map>) {
+
+ /**
+ * Returns the first header value for the given name, or null if none.
+ *
+ * @param name the header name (case-insensitive)
+ * @return the first header value, or null if not found
+ * @throws IllegalArgumentException if [name] is null
+ */
+ fun get(name: String): String? = headersMap[name.lowercase(Locale.US)]?.firstOrNull()
+
+ /**
+ * Returns all header values for the given name.
+ *
+ * @param name the header name (case-insensitive)
+ * @return an unmodifiable list of header values, or an empty list if none
+ * @throws IllegalArgumentException if [name] is null
+ */
+ fun values(name: String): List = headersMap[name.lowercase(Locale.US)] ?: emptyList()
+
+ /**
+ * Returns an unmodifiable set of all header names.
+ *
+ * @return an unmodifiable set of header names
+ */
+ fun names(): Set = headersMap.keys
+
+ /**
+ * Returns an unmodifiable list of all header entries.
+ *
+ * @return an unmodifiable list of header entries as [Map.Entry]
+ */
+ fun entries(): Set>> = headersMap.entries
+
+ /**
+ * Returns a new [Builder] initialized with the existing headers.
+ *
+ * @return a new [Builder]
+ */
+ fun newBuilder(): Builder = Builder(this)
+
+ override fun toString(): String = headersMap.toString()
+
+ /**
+ * Builder for constructing [Headers] instances.
+ */
+ class Builder {
+
+ private val headersMap: MutableMap> = LinkedHashMap()
+
+ /**
+ * Creates a new builder
+ */
+ constructor()
+
+ /**
+ * Creates a new builder initialized with the headers from [headers].
+ *
+ * @param headers the headers to initialize from
+ */
+ constructor(headers: Headers) : this() {
+ headers.headersMap.forEach { (key, values) ->
+ headersMap[key] = values.toMutableList()
+ }
+ }
+
+ /**
+ * Adds a header with the specified name and value.
+ * Multiple headers with the same name are allowed.
+ *
+ * @param name the header name
+ * @param value the header value
+ * @return this builder
+ * @throws IllegalArgumentException if [name] or [value] is invalid
+ */
+ @Throws(IllegalArgumentException::class)
+ fun add(name: String, value: String): Builder = apply { add(name, listOf(value)) }
+
+ /**
+ * Adds all header values for the specified name.
+ *
+ * @param name the header name
+ * @param values the list of header values
+ * @return this builder
+ * @throws IllegalArgumentException if [name] or any [values] are invalid
+ */
+ @Throws(IllegalArgumentException::class)
+ fun add(name: String, values: List): Builder = apply {
+ headersMap.computeIfAbsent(name) { mutableListOf() }.addAll(values)
+ }
+
+ /**
+ * Sets the header with the specified name to the single value provided.
+ * If headers with this name already exist, they are removed.
+ *
+ * @param name the header name
+ * @param value the header value
+ * @return this builder
+ * @throws IllegalArgumentException if [name] or [value] is invalid
+ */
+ @Throws(IllegalArgumentException::class)
+ fun set(name: String, value: String): Builder = apply { set(name, listOf(value)) }
+
+ /**
+ * Sets the header with the specified name to the values list provided.
+ * If headers with this name already exist, they are removed.
+ *
+ * @param name the header name
+ * @param values the header value
+ * @return this builder
+ * @throws IllegalArgumentException if [name] or [values] are invalid
+ */
+ @Throws(IllegalArgumentException::class)
+ fun set(name: String, values: List): Builder = apply {
+ remove(name)
+ add(name, values)
+ }
+
+ /**
+ * Removes any header with the specified name.
+ *
+ * @param name the header name
+ * @return this builder
+ */
+ fun remove(name: String): Builder = apply {
+ headersMap.remove(name.lowercase(Locale.US))
+ }
+
+ /**
+ * Builds an immutable [Headers] instance.
+ *
+ * @return the built [Headers]
+ */
+ fun build(): Headers {
+ return Headers(LinkedHashMap(headersMap))
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/MediaType.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/MediaType.kt
new file mode 100644
index 00000000..8ca073a4
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/MediaType.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.http
+
+import java.nio.charset.Charset
+import java.nio.charset.UnsupportedCharsetException
+import java.util.Locale
+
+/**
+ * Represents a media type, as defined in the HTTP specification.
+ *
+ * @property type The primary type (e.g., "application", "text").
+ * @property subtype The subtype (e.g., "json", "plain").
+ * @property parameters The map of parameters associated with the media type (e.g., charset).
+ */
+class MediaType(
+ val type: String,
+ val subtype: String,
+ val parameters: Map = emptyMap()
+) {
+
+ /**
+ * The full type of the media type, consisting of [type]/[subtype].
+ */
+ val fullType: String
+ get() = "$type/$subtype"
+
+ /**
+ * Retrieves the character set from the parameters, if present.
+ */
+ val charset: Charset?
+ get() = parameters["charset"]?.let {
+ try {
+ Charset.forName(it)
+ } catch (_: UnsupportedCharsetException) {
+ null
+ }
+ }
+
+ /**
+ * Checks if this media type includes the given media type.
+ *
+ * @param other The media type to compare against.
+ * @return `true` if this media type includes the given media type, `false` otherwise.
+ */
+ fun includes(other: MediaType): Boolean {
+ if (this.type == "*") {
+ return true
+ } else if (this.type.equals(other.type, ignoreCase = true)) {
+ if (this.subtype == "*" || this.subtype.equals(other.subtype, ignoreCase = true)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns a copy of this `MediaType` with the given charset parameter.
+ *
+ * @param charset The charset to set.
+ * @return A new `MediaType` instance with the specified charset.
+ */
+ fun withCharset(charset: Charset): MediaType {
+ val newParameters = parameters.toMutableMap()
+ newParameters["charset"] = charset.name()
+ return MediaType(type, subtype, newParameters)
+ }
+
+ override fun toString(): String {
+ val params = parameters.entries.joinToString(separator = ";") { (key, value) ->
+ "$key=$value"
+ }
+ return if (params.isNotEmpty()) "$type/$subtype;$params" else "$type/$subtype"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is MediaType) return false
+
+ return type.equals(other.type, ignoreCase = true) &&
+ subtype.equals(other.subtype, ignoreCase = true) &&
+ parameters.entries.all { (key, value) ->
+ other.parameters[key]?.equals(value, ignoreCase = true) == true
+ }
+ }
+
+ override fun hashCode(): Int {
+ var result = type.lowercase(Locale.getDefault()).hashCode()
+ result = 31 * result + subtype.lowercase(Locale.getDefault()).hashCode()
+ result = 31 * result + parameters.mapKeys { it.key.lowercase(Locale.getDefault()) }.hashCode()
+ return result
+ }
+
+ companion object {
+ /**
+ * Parses a media type string into a [MediaType] object.
+ *
+ * @param mediaType The media type string to parse.
+ * @return The parsed [MediaType].
+ * @throws IllegalArgumentException If the media type cannot be parsed.
+ */
+ fun parse(mediaType: String): MediaType {
+ require(mediaType.isNotBlank()) { "Media type must not be blank" }
+
+ val parts = mediaType.split(";").map { it.trim() }
+ val typeSubtype = parts[0].split("/")
+
+ if (typeSubtype.size != 2) {
+ throw IllegalArgumentException("Invalid media type format: $mediaType")
+ }
+
+ val type = typeSubtype[0].lowercase(Locale.getDefault())
+ val subtype = typeSubtype[1].lowercase(Locale.getDefault())
+
+ val parameters = mutableMapOf()
+ for (i in 1 until parts.size) {
+ val parameter = parts[i]
+ val idx = parameter.indexOf('=')
+ if (idx == -1) {
+ throw IllegalArgumentException("Invalid parameter in media type: $parameter")
+ }
+ val name = parameter.substring(0, idx).trim().lowercase(Locale.getDefault())
+ val value = parameter.substring(idx + 1).trim().trim('"')
+ parameters[name] = value
+ }
+
+ return MediaType(type, subtype, parameters)
+ }
+
+ /** Common media type constants **/
+ val ALL = MediaType("*", "*")
+ val APPLICATION_JSON = MediaType("application", "json")
+ val APPLICATION_XML = MediaType("application", "xml")
+ val TEXT_PLAIN = MediaType("text", "plain")
+ val TEXT_HTML = MediaType("text", "html")
+ val APPLICATION_OCTET_STREAM = MediaType("application", "octet-stream")
+ val MULTIPART_FORM_DATA = MediaType("multipart", "form-data")
+ val APPLICATION_FORM_URLENCODED = MediaType("application", "x-www-form-urlencoded")
+
+ /**
+ * Parses multiple media types from a comma-separated string.
+ *
+ * @param mediaTypes The string containing comma-separated media types.
+ * @return A list of parsed [MediaType] objects.
+ */
+ fun parseMediaTypes(mediaTypes: String): List {
+ return mediaTypes.split(",").map { parse(it.trim()) }
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/Protocol.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Protocol.kt
new file mode 100644
index 00000000..8cdb53c5
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Protocol.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.http;
+
+/**
+ * Enumeration of HTTP protocols.
+ */
+enum class Protocol {
+ HTTP_1_0,
+ HTTP_1_1,
+ HTTP_2,
+ H2_PRIOR_KNOWLEDGE,
+ QUIC;
+
+ companion object {
+ /**
+ * Parses a protocol string to a [Protocol] enum.
+ */
+ fun get(protocol: String): Protocol = when (protocol.uppercase()) {
+ "HTTP/1.0" -> HTTP_1_0
+ "HTTP/1.1" -> HTTP_1_1
+ "HTTP/2", "HTTP/2.0" -> HTTP_2
+ "H2_PRIOR_KNOWLEDGE" -> H2_PRIOR_KNOWLEDGE
+ "QUIC" -> QUIC
+ else -> throw IllegalArgumentException("Unexpected protocol: $protocol")
+ }
+ }
+
+ override fun toString(): String = when (this) {
+ HTTP_1_0 -> "http/1.0"
+ HTTP_1_1 -> "http/1.1"
+ HTTP_2 -> "http/2"
+ H2_PRIOR_KNOWLEDGE -> "h2_prior_knowledge"
+ QUIC -> "quic"
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/Request.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Request.kt
new file mode 100644
index 00000000..65d9ce06
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Request.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.http
+
+import java.util.Locale
+
+/**
+ * Represents an immutable HTTP request.
+ *
+ * Use [Builder] to create an instance.
+ */
+class Request private constructor(
+ val method: String,
+ val url: Url,
+ val headers: Headers,
+ val body: RequestBody?,
+ val tags: Map, Any>
+) {
+
+ /**
+ * Returns the tag associated with the given [type], or null if none.
+ *
+ * @param type The class type of the tag.
+ * @return The tag object, or null if none.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun tag(type: Class): T? = tags[type] as? T
+
+ /**
+ * Returns the tag associated with [Any] as key, or null if none.
+ *
+ * @return The tag object, or null if none.
+ */
+ fun tag(): Any? = tag(Any::class.java)
+
+ /**
+ * Returns a new [Builder] initialized with this request's data.
+ *
+ * @return A new builder.
+ */
+ fun newBuilder(): Builder = Builder(this)
+
+ /**
+ * Builder class for [Request].
+ */
+ class Builder {
+ private var method: String? = null
+ private var url: Url? = null
+ private var headers: Headers.Builder = Headers.Builder()
+ private var body: RequestBody? = null
+ private var tags: MutableMap, Any> = mutableMapOf()
+
+ /**
+ * Creates a new builder.
+ */
+ constructor()
+
+ /**
+ * Creates a builder initialized with the data from [request].
+ *
+ * @param request The request to copy data from.
+ */
+ constructor(request: Request) {
+ this.method = request.method
+ this.url = request.url
+ this.headers = request.headers.newBuilder()
+ this.body = request.body
+ this.tags = request.tags.toMutableMap()
+ }
+
+ /**
+ * Sets the HTTP method.
+ *
+ * @param method HTTP method, e.g., GET, POST.
+ * @param body Optional request body.
+ * @return This builder.
+ * @throws IllegalArgumentException If [method] is empty.
+ */
+ fun method(method: String, body: RequestBody? = null) = apply {
+ require(method.isNotEmpty()) { "Method cannot be empty" }
+ val upperMethod = method.uppercase(Locale.US)
+ this.method = upperMethod
+ this.body = body
+ }
+
+ /**
+ * Sets the URL.
+ *
+ * @param url The URL as a string.
+ * @return This builder.
+ * @throws IllegalArgumentException If [url] is invalid.
+ */
+ fun url(url: String) = apply {
+ val parsedUrl = Url.parse(url) ?: throw IllegalArgumentException("Invalid URL: $url")
+ this.url = parsedUrl
+ }
+
+ /**
+ * Sets the URL.
+ *
+ * @param url The URL as an [Url] object.
+ * @return This builder.
+ */
+ fun url(url: Url) = apply {
+ this.url = url
+ }
+
+ /**
+ * Adds a header with the specified name and value.
+ *
+ * @param name The header name.
+ * @param value The header value.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [value] is invalid.
+ */
+ fun addHeader(name: String, value: String) = apply {
+ headers.add(name, value)
+ }
+
+ /**
+ * Adds a header with the specified name and values.
+ *
+ * @param name The header name.
+ * @param values The header values list.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [values] are invalid.
+ */
+ fun addHeader(name: String, values: List) = apply {
+ headers.add(name, values)
+ }
+
+ /**
+ * Sets a header with the specified name and value, replacing any existing values.
+ *
+ * @param name The header name.
+ * @param value The header value.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [value] is invalid.
+ */
+ fun header(name: String, value: String) = apply {
+ headers.set(name, value)
+ }
+
+ /**
+ * Sets a header with the specified name and values list, replacing any existing values.
+ *
+ * @param name The header name.
+ * @param values The header values list.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [values] are invalid.
+ */
+ fun header(name: String, values: List) = apply {
+ headers.set(name, values)
+ }
+
+ /**
+ * Removes all headers with the specified name.
+ *
+ * @param name The header name.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] is null.
+ */
+ fun removeHeader(name: String) = apply {
+ headers.remove(name)
+ }
+
+ /**
+ * Sets the request body.
+ *
+ * @param body The request body.
+ * @return This builder.
+ */
+ fun body(body: RequestBody) = apply {
+ this.body = body
+ }
+
+ /**
+ * Adds a tag to the request.
+ *
+ * @param type The class type of the tag.
+ * @param tag The tag object, or null to remove it.
+ * @return This builder.
+ * @throws IllegalArgumentException If [type] is null.
+ */
+ fun tag(type: Class, tag: T?) = apply {
+ if (tag == null) {
+ tags.remove(type)
+ } else {
+ tags[type] = tag
+ }
+ }
+
+ /**
+ * Builds the [Request].
+ *
+ * @return The built request.
+ * @throws IllegalStateException If the request is invalid.
+ */
+ fun build(): Request {
+ val method = this.method ?: throw IllegalStateException("Method is required.")
+ val url = this.url ?: throw IllegalStateException("URL is required.")
+
+ return Request(
+ method = method,
+ url = url,
+ headers = headers.build(),
+ body = body,
+ tags = tags
+ )
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/RequestBody.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/RequestBody.kt
new file mode 100644
index 00000000..85692c9c
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/RequestBody.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.http
+
+import java.io.IOException
+import java.io.InputStream
+import java.net.URLEncoder
+import java.nio.charset.Charset
+import okio.BufferedSink
+import okio.Source
+import okio.source
+
+/**
+ * Represents an HTTP request body.
+ */
+abstract class RequestBody {
+
+ /**
+ * Returns the media type of the request body.
+ */
+ abstract fun mediaType(): MediaType?
+
+ /**
+ * Returns the number of bytes that will be written to [writeTo], or -1 if unknown.
+ */
+ open fun contentLength(): Long = -1
+
+ /**
+ * Writes the request body to the given [sink].
+ *
+ * @param sink the sink to write to.
+ * @throws IOException if an I/O error occurs.
+ */
+ @Throws(IOException::class)
+ abstract fun writeTo(sink: BufferedSink)
+
+ companion object {
+ /**
+ * Creates a new request body that reads from the given [inputStream].
+ *
+ * @param mediaType the media type, or null if unknown.
+ * @param contentLength the length of the content, or -1 if unknown.
+ * @param inputStream the input stream to read from.
+ * @return a new [RequestBody] instance.
+ */
+ fun create(
+ inputStream: InputStream,
+ mediaType: MediaType? = null,
+ contentLength: Long = -1
+ ): RequestBody {
+ return object : RequestBody() {
+ override fun mediaType(): MediaType? = mediaType
+
+ override fun contentLength(): Long = contentLength
+
+ @Throws(IOException::class)
+ override fun writeTo(sink: BufferedSink) {
+ inputStream.use {
+ sink.writeAll(it.source())
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a new request body that reads from the given [source].
+ *
+ * @param mediaType the media type, or null if unknown.
+ * @param contentLength the length of the content, or -1 if unknown.
+ * @param source the source to read from.
+ * @return a new [RequestBody] instance.
+ */
+ fun create(
+ source: Source,
+ mediaType: MediaType? = null,
+ contentLength: Long = -1
+ ): RequestBody {
+ return object : RequestBody() {
+ override fun mediaType(): MediaType? = mediaType
+
+ override fun contentLength(): Long = contentLength
+
+ @Throws(IOException::class)
+ override fun writeTo(sink: BufferedSink) {
+ source.use { src ->
+ sink.writeAll(src)
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a new request body for form data with content type "application/x-www-form-urlencoded".
+ *
+ * @param formData The form data as a map of parameter names and values.
+ * @param charset The character set to use; defaults to UTF-8.
+ * @return A new [RequestBody] instance.
+ * @throws IllegalArgumentException If [formData] is null.
+ */
+ fun create(
+ formData: Map,
+ charset: Charset = Charsets.UTF_8
+ ): RequestBody {
+
+ val encodedForm = formData.map { (key, value) ->
+ "${encode(key, charset)}=${encode(value, charset)}"
+ }.joinToString("&")
+
+ val contentBytes = encodedForm.toByteArray(charset)
+
+ return create(contentBytes.inputStream(), MediaType.APPLICATION_FORM_URLENCODED)
+ }
+
+ private fun encode(value: String, charset: Charset): String {
+ return URLEncoder.encode(value, charset.name())
+ .replace("+", "%20")
+ .replace("*", "%2A")
+ .replace("%7E", "~")
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/Response.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Response.kt
new file mode 100644
index 00000000..a707796a
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Response.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.http
+
+import java.io.Closeable
+import java.io.IOException
+
+/**
+ * Represents an immutable HTTP response.
+ *
+ * Use [Builder] to create an instance.
+ */
+class Response private constructor(
+ val request: Request,
+ val protocol: Protocol,
+ val code: Int,
+ val message: String,
+ val headers: Headers,
+ val body: ResponseBody?
+) : Closeable {
+
+ /**
+ * Returns the header value for [name], or null if not present.
+ *
+ * @param name The name of the header.
+ * @return The value of the header, or null if not present.
+ */
+ fun header(name: String): String? = headers.get(name)
+
+ /**
+ * Returns all header values for [name].
+ *
+ * @param name The name of the header.
+ * @return A list of header values.
+ */
+ fun headers(name: String): List = headers.values(name)
+
+ /**
+ * Returns true if the response code is in the 200-299 range.
+ */
+ val isSuccessful: Boolean
+ get() = code in 200..299
+
+ /**
+ * Returns a new [Builder] initialized with this response's data.
+ *
+ * @return A new builder.
+ */
+ fun newBuilder(): Builder = Builder(this)
+
+ /**
+ * Closes the response body and releases any resources.
+ *
+ * After calling this method, the response body cannot be read.
+ *
+ * @throws IOException If an I/O error occurs.
+ */
+ @Throws(IOException::class)
+ override fun close() {
+ body?.close()
+ }
+
+ /**
+ * Builder class for [Response].
+ */
+ class Builder {
+ private var request: Request? = null
+ private var protocol: Protocol? = null
+ private var code: Int = -1
+ private var message: String? = null
+ private var headers: Headers.Builder = Headers.Builder()
+ private var body: ResponseBody? = null
+
+ /**
+ * Creates an empty builder.
+ */
+ constructor()
+
+ /**
+ * Creates a builder initialized with the data from [response].
+ *
+ * @param response The response to copy data from.
+ */
+ constructor(response: Response) {
+ this.request = response.request
+ this.protocol = response.protocol
+ this.code = response.code
+ this.message = response.message
+ this.headers = response.headers.newBuilder()
+ this.body = response.body
+ }
+
+ /**
+ * Sets the request that initiated this response.
+ *
+ * @param request The originating request.
+ * @return This builder.
+ */
+ fun request(request: Request) = apply {
+ this.request = request
+ }
+
+ /**
+ * Sets the protocol used for the response.
+ *
+ * @param protocol The protocol (e.g., HTTP/1.1).
+ * @return This builder.
+ */
+ fun protocol(protocol: Protocol) = apply {
+ this.protocol = protocol
+ }
+
+ /**
+ * Sets the HTTP status code.
+ *
+ * @param code The HTTP status code.
+ * @return This builder.
+ * @throws IllegalArgumentException If [code] is negative.
+ */
+ fun code(code: Int) = apply {
+ require(code >= 0) { "code must be >= 0" }
+ this.code = code
+ }
+
+ /**
+ * Sets the HTTP reason phrase.
+ *
+ * @param message The reason phrase.
+ * @return This builder.
+ */
+ fun message(message: String) = apply {
+ this.message = message
+ }
+
+ /**
+ * Adds a header with the specified name and value.
+ *
+ * @param name The header name.
+ * @param value The header value.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [value] is invalid.
+ */
+ fun addHeader(name: String, value: String) = apply {
+ headers.add(name, value)
+ }
+
+ /**
+ * Adds a header with the specified name and values list.
+ *
+ * @param name The header name.
+ * @param values The header value.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [values] are invalid.
+ */
+ fun addHeader(name: String, values: List) = apply {
+ headers.add(name, values)
+ }
+
+ /**
+ * Sets a header with the specified name and value, replacing any existing values.
+ *
+ * @param name The header name.
+ * @param value The header value.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [value] is invalid.
+ */
+ fun header(name: String, value: String) = apply {
+ headers.set(name, value)
+ }
+
+ /**
+ * Sets a header with the specified name and values list, replacing any existing values.
+ *
+ * @param name The header name.
+ * @param values The header values list.
+ * @return This builder.
+ * @throws IllegalArgumentException If [name] or [values] are invalid.
+ */
+ fun header(name: String, values: List) = apply {
+ headers.set(name, values)
+ }
+
+ /**
+ * Removes all headers with the specified name.
+ *
+ * @param name The header name.
+ * @return This builder.
+ */
+ fun removeHeader(name: String) = apply {
+ headers.remove(name)
+ }
+
+ /**
+ * Sets the response headers.
+ *
+ * @param headers The response headers.
+ * @return This builder.
+ */
+ fun headers(headers: Headers) = apply {
+ this.headers = headers.newBuilder()
+ }
+
+ /**
+ * Sets the response body.
+ *
+ * @param body The response body, or null if none.
+ * @return This builder.
+ */
+ fun body(body: ResponseBody?) = apply {
+ this.body = body
+ }
+
+ /**
+ * Builds the [Response].
+ *
+ * @return The built response.
+ * @throws IllegalStateException If required fields are missing.
+ */
+ fun build(): Response {
+ val request = this.request ?: throw IllegalStateException("request is required")
+ val protocol = this.protocol ?: throw IllegalStateException("protocol is required")
+ val code = this.code.takeIf { it >= 0 } ?: throw IllegalStateException("code is required")
+ val message = this.message ?: throw IllegalStateException("message is required")
+
+ return Response(
+ request = request,
+ protocol = protocol,
+ code = code,
+ message = message,
+ headers = headers.build(),
+ body = body
+ )
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/ResponseBody.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/ResponseBody.kt
new file mode 100644
index 00000000..ce9c0f40
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/ResponseBody.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.http
+
+import java.io.Closeable
+import java.io.InputStream
+import okio.BufferedSource
+import okio.IOException
+import okio.buffer
+import okio.source
+
+/**
+ * Represents the body of an HTTP response.
+ */
+abstract class ResponseBody : Closeable {
+
+ /**
+ * Returns the media type of the response body, or null if unknown.
+ */
+ abstract fun mediaType(): MediaType?
+
+ /**
+ * Returns the content length, or -1 if unknown.
+ */
+ abstract fun contentLength(): Long
+
+ /**
+ * Returns a [BufferedSource] to read the response body.
+ *
+ * Note: The source can be read only once. Multiple calls will return the same source.
+ *
+ * @return The buffered source.
+ */
+ abstract fun source(): BufferedSource
+
+ /**
+ * Closes the response body and releases any resources.
+ *
+ * @throws IOException If an I/O error occurs.
+ */
+ @Throws(IOException::class)
+ override fun close() {
+ source().close()
+ }
+
+ companion object {
+ /**
+ * Creates a new response body from an [InputStream] and [mediaType].
+ *
+ * @param inputStream The input stream to read from.
+ * @param contentLength The length of the content, or -1 if unknown.
+ * @param mediaType The media type, or null if unknown.
+ * @return A new [ResponseBody] instance.
+ */
+ fun create(
+ inputStream: InputStream,
+ mediaType: MediaType? = null,
+ contentLength: Long = -1L
+ ): ResponseBody {
+ return object : ResponseBody() {
+ private val source = inputStream.source().buffer()
+
+ override fun mediaType(): MediaType? = mediaType
+
+ override fun contentLength(): Long = contentLength
+
+ override fun source(): BufferedSource = source
+ }
+ }
+
+ /**
+ * Creates a new response body from a [BufferedSource] and [mediaType].
+ *
+ * @param source The buffered source to read from.
+ * @param contentLength The length of the content, or -1 if unknown.
+ * @param mediaType The media type, or null if unknown.
+ * @return A new [ResponseBody] instance.
+ */
+ fun create(source: BufferedSource, mediaType: MediaType? = null, contentLength: Long = -1L): ResponseBody {
+ return object : ResponseBody() {
+ override fun mediaType(): MediaType? = mediaType
+
+ override fun contentLength(): Long = contentLength
+
+ override fun source(): BufferedSource = source
+ }
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/Status.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Status.kt
new file mode 100644
index 00000000..51839259
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Status.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.http
+
+import com.expediagroup.sdk.core.http.Status.entries
+
+enum class Status(val code: Int) {
+ // Informational responses (100–199)
+ CONTINUE(100),
+ SWITCHING_PROTOCOLS(101),
+ PROCESSING(102),
+ EARLY_HINTS(103),
+
+ // Successful responses (200–299)
+ OK(200),
+ CREATED(201),
+ ACCEPTED(202),
+ NON_AUTHORITATIVE_INFORMATION(203),
+ NO_CONTENT(204),
+ RESET_CONTENT(205),
+ PARTIAL_CONTENT(206),
+ MULTI_STATUS(207),
+ ALREADY_REPORTED(208),
+ IM_USED(226),
+
+ // Redirection messages (300–399)
+ MULTIPLE_CHOICES(300),
+ MOVED_PERMANENTLY(301),
+ FOUND(302),
+ SEE_OTHER(303),
+ NOT_MODIFIED(304),
+ USE_PROXY(305),
+ TEMPORARY_REDIRECT(307),
+ PERMANENT_REDIRECT(308),
+
+ // Client error responses (400–499)
+ BAD_REQUEST(400),
+ UNAUTHORIZED(401),
+ PAYMENT_REQUIRED(402),
+ FORBIDDEN(403),
+ NOT_FOUND(404),
+ METHOD_NOT_ALLOWED(405),
+ NOT_ACCEPTABLE(406),
+ PROXY_AUTHENTICATION_REQUIRED(407),
+ REQUEST_TIMEOUT(408),
+ CONFLICT(409),
+ GONE(410),
+ LENGTH_REQUIRED(411),
+ PRECONDITION_FAILED(412),
+ PAYLOAD_TOO_LARGE(413),
+ URI_TOO_LONG(414),
+ UNSUPPORTED_MEDIA_TYPE(415),
+ RANGE_NOT_SATISFIABLE(416),
+ EXPECTATION_FAILED(417),
+ IM_A_TEAPOT(418),
+ MISDIRECTED_REQUEST(421),
+ UNPROCESSABLE_ENTITY(422),
+ LOCKED(423),
+ FAILED_DEPENDENCY(424),
+ TOO_EARLY(425),
+ UPGRADE_REQUIRED(426),
+ PRECONDITION_REQUIRED(428),
+ TOO_MANY_REQUESTS(429),
+ REQUEST_HEADER_FIELDS_TOO_LARGE(431),
+ UNAVAILABLE_FOR_LEGAL_REASONS(451),
+
+ // Server error responses (500–599)
+ INTERNAL_SERVER_ERROR(500),
+ NOT_IMPLEMENTED(501),
+ BAD_GATEWAY(502),
+ SERVICE_UNAVAILABLE(503),
+ GATEWAY_TIMEOUT(504),
+ HTTP_VERSION_NOT_SUPPORTED(505),
+ VARIANT_ALSO_NEGOTIATES(506),
+ INSUFFICIENT_STORAGE(507),
+ LOOP_DETECTED(508),
+ NOT_EXTENDED(510),
+ NETWORK_AUTHENTICATION_REQUIRED(511),
+
+ // Non-standard status codes (e.g., Apache)
+ THIS_IS_FINE(218); // Non-standard code, used by some Apache modules
+
+ companion object {
+
+ @Throws(IllegalArgumentException::class)
+ fun fromCode(code: Int): Status {
+ entries.find { it.code == code }?.let {
+ return it
+ }
+
+ throw IllegalArgumentException("Invalid status code: $code")
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/http/Url.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Url.kt
new file mode 100644
index 00000000..ae8bd80d
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/http/Url.kt
@@ -0,0 +1,326 @@
+/*
+ * 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.http
+
+import com.expediagroup.sdk.core.http.Url.Companion.parse
+import java.net.IDN
+import java.net.InetAddress
+import java.net.MalformedURLException
+import java.net.URI
+import java.net.URISyntaxException
+import java.net.URL
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+import java.util.Locale
+
+/**
+ * Represents an immutable URL with proper encoding and parsing.
+ *
+ * The [Url] class provides functionality for creating, validating, and manipulating URLs. It ensures that
+ * URLs are correctly encoded, parsed, and represented as standardized strings or objects like [URI] and [URL].
+ *
+ * Instances of [Url] are immutable and must be created using the [Builder] or the [parse] method.
+ */
+class Url private constructor(
+ private val scheme: String,
+ private val userInfo: String?,
+ private val host: String,
+ private val port: Int,
+ private val encodedPath: String,
+ private val encodedQuery: String?,
+ private val fragment: String?
+) {
+
+ /**
+ * Returns the URL as a properly formatted string.
+ * The string representation includes the scheme, user info, host, port, path, query, and fragment.
+ */
+ override fun toString(): String = buildString {
+ append(scheme)
+ append("://")
+ if (userInfo != null) {
+ append(userInfo)
+ append("@")
+ }
+ append(hostToString(host))
+ if (port != defaultPort(scheme)) {
+ append(":")
+ append(port)
+ }
+ append(encodedPath)
+ if (encodedQuery != null) {
+ append("?")
+ append(encodedQuery)
+ }
+ if (fragment != null) {
+ append("#")
+ append(fragment)
+ }
+ }
+
+ /**
+ * Converts the URL to a [URI] object.
+ *
+ * @return A [URI] representation of the URL.
+ * @throws URISyntaxException
+ */
+ @Throws(URISyntaxException::class)
+ fun toUri(): URI = URI(
+ scheme,
+ userInfo,
+ host,
+ if (port != defaultPort(scheme)) port else -1,
+ encodedPath,
+ encodedQuery,
+ fragment
+ )
+
+ /**
+ * Converts the URL to a [URL] object.
+ *
+ * @return A [URL] representation of the URL.
+ * @throws MalformedURLException
+ */
+ @Throws(MalformedURLException::class)
+ fun toUrl(): URL = toUri().toURL()
+
+ /**
+ * Builder for creating and constructing [Url] instances.
+ *
+ * The [Builder] class provides a fluent API for incrementally specifying the components of a URL, such as
+ * the scheme, host, port, path segments, query parameters, and fragment. It ensures that all components
+ * are properly validated and encoded.
+ */
+ class Builder {
+ private var scheme: String? = null
+ private var userInfo: String? = null
+ private var host: String? = null
+ private var port: Int? = null
+ private val pathSegments: MutableList = mutableListOf()
+ private val queryParameters: MutableList> = mutableListOf()
+ private var fragment: String? = null
+
+ /**
+ * Sets the URL scheme (e.g., "http", "https").
+ *
+ * @param scheme The scheme to use for the URL.
+ * @return The [Builder] instance for chaining.
+ * @throws IllegalArgumentException If the scheme is invalid.
+ */
+ fun scheme(scheme: String) = apply {
+ require(scheme.matches(Regex("^[a-zA-Z][a-zA-Z0-9+\\-.]*$"))) {
+ "Invalid URL scheme: $scheme"
+ }
+ this.scheme = scheme.lowercase(Locale.US)
+ }
+
+ /**
+ * Sets the user info (e.g., "user:password").
+ *
+ * @param userInfo The user info to include in the URL.
+ * @return The [Builder] instance for chaining.
+ */
+ fun userInfo(userInfo: String) = apply {
+ this.userInfo = userInfo
+ }
+
+ /**
+ * Sets the host of the URL.
+ *
+ * @param host The hostname or IP address.
+ * @return The [Builder] instance for chaining.
+ * @throws IllegalArgumentException If the host is invalid.
+ */
+ fun host(host: String) = apply {
+ require(isValidHost(host)) { "Invalid host: $host" }
+ this.host = host
+ }
+
+ /**
+ * Sets the port of the URL.
+ *
+ * @param port The port number.
+ * @return The [Builder] instance for chaining.
+ * @throws IllegalArgumentException If the port is out of range (1–65535).
+ */
+ fun port(port: Int) = apply {
+ require(port in 1..65535) { "Invalid port: $port" }
+ this.port = port
+ }
+
+ /**
+ * Adds a path segment to the URL.
+ *
+ * @param segment The path segment to add.
+ * @return The [Builder] instance for chaining.
+ */
+ fun addPathSegment(segment: String) = apply {
+ pathSegments.add(encodePathSegment(segment))
+ }
+
+ /**
+ * Adds a query parameter to the URL.
+ *
+ * @param name The query parameter name.
+ * @param value The query parameter value.
+ * @return The [Builder] instance for chaining.
+ */
+ fun addQueryParameter(name: String, value: String) = apply {
+ queryParameters.add(encodeQueryParameter(name) to encodeQueryParameter(value))
+ }
+
+ /**
+ * Sets the fragment of the URL.
+ *
+ * @param fragment The fragment identifier.
+ * @return The [Builder] instance for chaining.
+ */
+ fun fragment(fragment: String) = apply {
+ this.fragment = encodeFragment(fragment)
+ }
+
+ /**
+ * Builds and returns a new [Url] instance.
+ *
+ * @return The constructed [Url].
+ * @throws IllegalStateException If required components (e.g., scheme or host) are missing.
+ */
+ fun build(): Url {
+ val scheme = this.scheme ?: throw IllegalStateException("Scheme is required.")
+ val host = this.host ?: throw IllegalStateException("Host is required.")
+ val port = this.port ?: defaultPort(scheme)
+ val encodedPath = buildEncodedPath()
+ val encodedQuery = buildEncodedQuery()
+ return Url(
+ scheme,
+ userInfo,
+ host,
+ port,
+ encodedPath,
+ encodedQuery,
+ fragment
+ )
+ }
+
+ private fun buildEncodedPath(): String {
+ return if (pathSegments.isEmpty()) {
+ "/"
+ } else {
+ pathSegments.joinToString("/", prefix = "/")
+ }
+ }
+
+ private fun buildEncodedQuery(): String? {
+ return if (queryParameters.isEmpty()) {
+ null
+ } else {
+ queryParameters.joinToString("&") { (name, value) ->
+ if (value != null) "$name=$value" else name
+ }
+ }
+ }
+
+ private fun encodePathSegment(segment: String): String {
+ return URLEncoder.encode(segment, StandardCharsets.UTF_8.name())
+ .replace("+", "%20")
+ .replace("%2F", "/")
+ }
+
+ private fun encodeQueryParameter(value: String): String {
+ return value.let {
+ URLEncoder.encode(it, StandardCharsets.UTF_8.name())
+ .replace("+", "%20")
+ }
+ }
+
+ private fun encodeFragment(fragment: String): String {
+ return URLEncoder.encode(fragment, StandardCharsets.UTF_8.name())
+ .replace("+", "%20")
+ }
+
+ private fun isValidHost(host: String): Boolean {
+ return try {
+ val idnHost = IDN.toASCII(host)
+ InetAddress.getByName(idnHost)
+ true
+ } catch (e: Exception) {
+ false
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Parses a URL string into an [Url] instance.
+ *
+ * This method parses the given string, extracting and validating its components (e.g., scheme, host, port, path).
+ *
+ * @param url The URL string to parse.
+ * @return A [Url] instance representing the parsed URL, or `null` if the URL is invalid.
+ * @throws URISyntaxException If the URL cannot be parsed due to syntax issues.
+ */
+ @Throws(URISyntaxException::class)
+ fun parse(url: String): Url? {
+ val uri = URI(url)
+ val scheme = uri.scheme ?: return null
+ val host = uri.host ?: return null
+ val port = if (uri.port != -1) uri.port else defaultPort(scheme)
+ val userInfo = uri.userInfo
+ val encodedPath = if (uri.rawPath != null && uri.rawPath.isNotEmpty()) uri.rawPath else "/"
+ val encodedQuery = uri.rawQuery
+ val fragment = uri.rawFragment
+
+ return Url(
+ scheme = scheme,
+ userInfo = userInfo,
+ host = host,
+ port = port,
+ encodedPath = encodedPath,
+ encodedQuery = encodedQuery,
+ fragment = fragment
+ )
+ }
+
+ /**
+ * Returns the default port for a given scheme.
+ *
+ * @param scheme The URL scheme (e.g., "http", "https").
+ * @return The default port for the scheme, or -1 if unknown.
+ */
+ private fun defaultPort(scheme: String): Int {
+ return when (scheme.lowercase(Locale.US)) {
+ "http" -> 80
+ "https" -> 443
+ else -> -1
+ }
+ }
+
+ /**
+ * Converts the host string to its proper string representation.
+ *
+ * @param host The hostname or IP address.
+ * @return The string representation of the host, with IPv6 addresses enclosed in brackets.
+ */
+ private fun hostToString(host: String): String {
+ return if (host.contains(":") && !host.startsWith("[")) {
+ "[$host]" // Enclose IPv6 addresses in brackets
+ } else {
+ host
+ }
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/interceptor/Interceptor.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/interceptor/Interceptor.kt
new file mode 100644
index 00000000..12890d49
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/interceptor/Interceptor.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.interceptor
+
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.Response
+import java.io.IOException
+
+/**
+ * Represents an interceptor for modifying or augmenting HTTP requests and responses within the SDK.
+ *
+ * An `Interceptor` allows pre-processing of requests and post-processing of responses as they pass
+ * through the HTTP execution pipeline. This can be used for various purposes such as logging, authentication,
+ * retry logic, or adding custom headers.
+ */
+interface Interceptor {
+ /**
+ * Intercepts an HTTP request and optionally modifies it or processes its corresponding response.
+ *
+ * Implementations of this method can inspect and modify the request before it is sent, or inspect
+ * and modify the response after it is received. The request is forwarded to the next interceptor in the chain
+ * by calling [Chain.proceed].
+ *
+ * @param chain The [Chain] containing the request to process and the logic to continue the chain.
+ * @return The [Response] resulting from the processed request.
+ * @throws IOException If an I/O error occurs during request execution or interception.
+ */
+ @Throws(IOException::class)
+ fun intercept(chain: Chain): Response
+
+ /**
+ * Represents a chain of interceptors and the ability to proceed with an HTTP request.
+ *
+ * Each interceptor in the chain can call [proceed] to pass the request to the next interceptor or
+ * handle the request and response directly.
+ */
+ interface Chain {
+ /**
+ * Retrieves the current HTTP request.
+ *
+ * @return The [Request] that is currently being processed.
+ */
+ fun request(): Request
+
+ /**
+ * Proceeds with the HTTP request, invoking the next interceptor in the chain or the final request execution.
+ *
+ * Interceptors use this method to pass the request down the chain. The returned response
+ * is the result of either the next interceptor or the HTTP client execution.
+ *
+ * @param request The [Request] to proceed with.
+ * @return The [Response] resulting from the request execution.
+ * @throws IOException If an I/O error occurs during request execution.
+ */
+ @Throws(IOException::class)
+ fun proceed(request: Request): Response
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/interceptor/InterceptorsChainExecutor.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/interceptor/InterceptorsChainExecutor.kt
new file mode 100644
index 00000000..42e731ed
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/interceptor/InterceptorsChainExecutor.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.interceptor
+
+import com.expediagroup.sdk.core.client.Transport
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.Response
+import java.io.IOException
+
+/**
+ * Executes a chain of [Interceptor] instances, ensuring sequential processing of HTTP requests and responses.
+ *
+ * The `InterceptorChainExecutor` is responsible for managing the flow of a request through a list of
+ * interceptors, ultimately delegating to the [Transport] for request execution when all interceptors
+ * have been processed.
+ *
+ * This class implements the [Interceptor.Chain] interface, providing methods for accessing the current
+ * request and proceeding to the next interceptor or the final request execution.
+ *
+ * @param interceptors The list of [Interceptor] instances to process.
+ * @param index The current position in the interceptor chain. Defaults to `0` for the first interceptor.
+ * @param request The [Request] being processed.
+ * @param transport The [Transport] responsible for executing the final HTTP request after all interceptors.
+ */
+class InterceptorsChainExecutor(
+ private val transport: Transport,
+ private val interceptors: List,
+ private var index: Int = 0,
+ private var request: Request
+) : Interceptor.Chain {
+
+ /**
+ * Retrieves the current [Request] being processed by the chain.
+ *
+ * @return The current [Request].
+ */
+ override fun request(): Request = request
+
+ /**
+ * Proceeds with the HTTP request by invoking the next [Interceptor] in the chain or
+ * executing the final request through the [Transport] if all interceptors have been processed.
+ *
+ * Each interceptor in the chain can modify the request or response as needed. If this is the last
+ * interceptor, the request is passed to the [Transport] for actual execution.
+ *
+ * @param request The [Request] to proceed with.
+ * @return The [Response] resulting from the next interceptor or the final HTTP client execution.
+ * @throws IOException If an I/O error occurs during request execution.
+ */
+ @Throws(IOException::class)
+ override fun proceed(request: Request): Response {
+ this.request = request
+
+ if (index >= interceptors.size) {
+ return transport.execute(request)
+ }
+
+ val interceptor = interceptors[index++]
+ return interceptor.intercept(this)
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/LoggingInterceptor.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/LoggingInterceptor.kt
new file mode 100644
index 00000000..7754c771
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/LoggingInterceptor.kt
@@ -0,0 +1,33 @@
+package com.expediagroup.sdk.core.logging
+
+import com.expediagroup.sdk.core.http.Response
+import com.expediagroup.sdk.core.interceptor.Interceptor
+import com.expediagroup.sdk.core.logging.common.Constant.DEFAULT_MAX_BODY_SIZE
+import com.expediagroup.sdk.core.logging.common.LoggerDecorator
+import com.expediagroup.sdk.core.logging.common.RequestLogger
+import com.expediagroup.sdk.core.logging.common.ResponseLogger
+import java.io.IOException
+
+/**
+ * An interceptor that logs HTTP requests and responses.
+ *
+ * @param maxBodyLogSize The maximum size of the request/response body to log. Defaults to 1MB.
+ */
+class LoggingInterceptor(
+ private val logger: LoggerDecorator,
+ private val maxBodyLogSize: Long = DEFAULT_MAX_BODY_SIZE
+) : Interceptor {
+
+ @Throws(IOException::class)
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+
+ RequestLogger.log(logger, request, maxBodyLogSize = maxBodyLogSize)
+
+ val response = chain.proceed(request)
+
+ ResponseLogger.log(logger, response, maxBodyLogSize = maxBodyLogSize)
+
+ return response
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/Constant.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/Constant.kt
new file mode 100644
index 00000000..dfa1f95f
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/Constant.kt
@@ -0,0 +1,8 @@
+package com.expediagroup.sdk.core.logging.common
+
+object Constant {
+ const val NEWLINE = "\n"
+ const val OMITTED = "<-- omitted -->"
+ const val DEFAULT_MAX_BODY_SIZE = 8192L //8KB
+ const val EXPEDIA_GROUP_SDK = "Expedia Group SDK"
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggableContentTypes.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggableContentTypes.kt
new file mode 100644
index 00000000..b6707b47
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggableContentTypes.kt
@@ -0,0 +1,24 @@
+package com.expediagroup.sdk.core.logging.common
+
+import com.expediagroup.sdk.core.http.MediaType
+
+/**
+ * A list of MIME types representing content types that are deemed loggable.
+ * This collection is used to determine whether the content of HTTP requests or responses
+ * can be logged based on their MIME types.
+ */
+val LOGGABLE_CONTENT_TYPES = listOf(
+ MediaType.APPLICATION_JSON,
+ MediaType.TEXT_PLAIN,
+ MediaType.TEXT_HTML,
+ MediaType.APPLICATION_XML,
+ MediaType.parse("text/*"), // Wildcard to include any text-based types
+ MediaType.parse("application/*+json"), // Matches custom JSON-based media types like application/.api+json
+ MediaType.APPLICATION_FORM_URLENCODED
+)
+
+internal fun isLoggable(mediaType: MediaType): Boolean {
+ return LOGGABLE_CONTENT_TYPES.any { loggableType ->
+ loggableType.includes(mediaType)
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggerDecorator.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggerDecorator.kt
new file mode 100644
index 00000000..e153ae81
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggerDecorator.kt
@@ -0,0 +1,35 @@
+package com.expediagroup.sdk.core.logging.common
+
+import org.slf4j.Logger
+
+class LoggerDecorator(private val logger: Logger) : Logger by logger {
+ override fun info(msg: String) = logger.info(decorate(msg))
+
+ override fun warn(msg: String) = logger.warn(decorate(msg))
+
+ override fun debug(msg: String) = logger.debug(decorate(msg))
+
+ override fun error(msg: String) = logger.error(decorate(msg))
+
+ override fun trace(msg: String) = logger.trace(decorate(msg))
+
+ fun info(msg: String, vararg tags: String) = logger.info(decorate(msg, tags.toSet()))
+
+ fun warn(msg: String, vararg tags: String) = logger.warn(decorate(msg, tags.toSet()))
+
+ fun debug(msg: String, vararg tags: String) = logger.debug(decorate(msg, tags.toSet()))
+
+ fun error(msg: String, vararg tags: String) = logger.error(decorate(msg, tags.toSet()))
+
+ fun trace(msg: String, vararg tags: String) = logger.trace(decorate(msg, tags.toSet()))
+
+ private fun decorate(msg: String, tags: Set? = null): String = buildString {
+ append("[${Constant.EXPEDIA_GROUP_SDK}] - ")
+ tags?.let {
+ append("[")
+ append(it.joinToString(", "))
+ append("] - ")
+ }
+ append(msg.trim())
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/RequestLogger.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/RequestLogger.kt
new file mode 100644
index 00000000..39fcf1ce
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/RequestLogger.kt
@@ -0,0 +1,48 @@
+package com.expediagroup.sdk.core.logging.common
+
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.RequestBody
+import com.expediagroup.sdk.core.logging.common.Constant.DEFAULT_MAX_BODY_SIZE
+import java.io.IOException
+import java.nio.charset.Charset
+import okio.Buffer
+
+object RequestLogger {
+ fun log(
+ logger: LoggerDecorator,
+ request: Request,
+ vararg tags: String,
+ maxBodyLogSize: Long = DEFAULT_MAX_BODY_SIZE
+ ) {
+ try {
+ val requestBodyString = request.body?.let { it.peekContent(maxBodyLogSize, it.mediaType()?.charset) }
+
+ buildString {
+ append("[URL=${request.url}, Method=${request.method}, Headers=[${request.headers}], Body=[${requestBodyString}]")
+ }.also {
+ logger.info(it, "Outgoing", *tags)
+ }
+
+ } catch (e: Exception) {
+ logger.warn("Failed to log request: ${e.message}", e)
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun RequestBody.peekContent(maxSize: Long, charset: Charset?): String {
+ this.mediaType().also {
+ if (it === null) {
+ return "Request body of unknown media type cannot be logged"
+ }
+
+ if (!isLoggable(it)) {
+ return "Request body of type ${it.fullType} cannot be logged"
+ }
+ }
+
+ val buffer = Buffer()
+ writeTo(buffer)
+ val bytesToRead = minOf(maxSize, buffer.size)
+ return buffer.copy().readString(bytesToRead, charset ?: Charsets.UTF_8)
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/ResponseLogger.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/ResponseLogger.kt
new file mode 100644
index 00000000..7a5c69d7
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/ResponseLogger.kt
@@ -0,0 +1,46 @@
+package com.expediagroup.sdk.core.logging.common
+
+import com.expediagroup.sdk.core.http.Response
+import com.expediagroup.sdk.core.http.ResponseBody
+import com.expediagroup.sdk.core.logging.common.Constant.DEFAULT_MAX_BODY_SIZE
+import java.nio.charset.Charset
+import okio.Buffer
+
+object ResponseLogger {
+ fun log(
+ logger: LoggerDecorator,
+ response: Response,
+ vararg tags: String,
+ maxBodyLogSize: Long = DEFAULT_MAX_BODY_SIZE
+ ) {
+ try {
+ val responseBodyString = response.body?.let { it.peekContent(maxBodyLogSize, it.mediaType()?.charset) }
+
+ buildString {
+ append("[URL=${response.request.url}, Code=${response.code}, Headers=[${response.headers}], Body=[${responseBodyString}]")
+ }.also {
+ logger.info(it, "Incoming", *tags)
+ }
+
+ } catch (e: Exception) {
+ logger.warn("Failed to log response: ${e.message}", e)
+ }
+ }
+
+ private fun ResponseBody.peekContent(maxSize: Long, charset: Charset?): String {
+ this.mediaType().also {
+ if (it === null) {
+ return "Response body of unknown media type cannot be logged"
+ }
+
+ if (!isLoggable(it)) {
+ return "Response body of type ${it.fullType} cannot be logged"
+ }
+ }
+
+ val buffer = Buffer()
+ val bytesToRead = minOf(maxSize, this.contentLength())
+ buffer.write(this.source().peek(), bytesToRead)
+ return buffer.copy().readString(charset ?: Charsets.UTF_8)
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldFilter.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldFilter.kt
new file mode 100644
index 00000000..b36f1b2a
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldFilter.kt
@@ -0,0 +1,19 @@
+package com.expediagroup.sdk.core.logging.masking
+
+import com.ebay.ejmask.core.BaseFilter
+
+/**
+ * A filter class that extends the BaseFilter to apply masking on specific JSON fields using
+ * `ExpediaGroupJsonFieldPatternBuilder` for pattern building.
+ *
+ * This filter helps in masking sensitive JSON fields by replacing them with a predefined pattern.
+ *
+ * @constructor
+ * Initializes ExpediaGroupJsonFieldFilter with the specified fields to be masked.
+ *
+ * @param maskedFields An array of strings representing the names of the fields to be masked.
+ */
+internal class JsonFieldFilter(maskedFields: Array) : BaseFilter(
+ JsonFieldPatternBuilder::class.java,
+ *maskedFields
+)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldPatternBuilder.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldPatternBuilder.kt
new file mode 100644
index 00000000..82543b08
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldPatternBuilder.kt
@@ -0,0 +1,17 @@
+package com.expediagroup.sdk.core.logging.masking
+
+import com.ebay.ejmask.extenstion.builder.json.JsonFieldPatternBuilder
+import com.expediagroup.sdk.core.logging.common.Constant.OMITTED
+
+/**
+ * A builder class for creating JSON field replacement patterns specifically for Expedia Group.
+ *
+ * This class extends the `JsonFieldPatternBuilder` and provides an implementation for building
+ * replacement patterns for JSON field masking.
+ *
+ * The replacement pattern format generated by this builder is structured to conceal sensitive
+ * data while keeping a specified number of characters visible.
+ */
+internal class JsonFieldPatternBuilder : JsonFieldPatternBuilder() {
+ override fun buildReplacement(visibleCharacters: Int, vararg fieldNames: String?): String = "\"$1$2$OMITTED\""
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskLogsUtils.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskLogsUtils.kt
new file mode 100644
index 00000000..eb2dfd51
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskLogsUtils.kt
@@ -0,0 +1,100 @@
+package com.expediagroup.sdk.core.logging.masking
+
+import com.ebay.ejmask.core.BaseFilter
+import com.ebay.ejmask.core.EJMask
+import com.ebay.ejmask.core.EJMaskInitializer
+import com.ebay.ejmask.core.util.LoggerUtil
+
+/**
+ * Masks sensitive information within the provided log string.
+ *
+ * @param logs The log string that may contain sensitive information requiring masking.
+ * @return A new log string with sensitive information masked.
+ */
+fun maskLogs(logs: String): String {
+ return MaskLogs.execute(logs)
+}
+
+/**
+ * Configures log masking by adding specified fields to the mask list.
+ *
+ * This function integrates with the log masking system to include additional fields
+ * that should be masked in the logs. The fields provided in the parameter are added
+ * to the set of fields that will have their values masked when logs are generated.
+ *
+ * @param fields The set of field names that need to be masked in the logs.
+ */
+fun configureLogMasking(fields: Set) {
+ MaskLogs.addFields(fields)
+}
+
+/**
+ * Checks if a specified field is among the fields that should be masked.
+ *
+ * @param field The name of the field to check.
+ * @return `true` if the field should be masked, `false` otherwise.
+ */
+fun isMaskedField(field: String): Boolean {
+ return MaskLogs.maskedFields.contains(field)
+}
+
+/**
+ * A utility class for masking sensitive information in log strings.
+ *
+ * The `MaskLogs` class is designed to replace sensitive information within logs with masked values.
+ * The class implements the `Function1` interface, enabling it to be invoked with a log string
+ * to produce a masked version of the string.
+ *
+ * The masking process relies on predefined filters that determine which fields within the log
+ * should be masked. Filters can be added and configured using the companion object's methods.
+ */
+private class MaskLogs : (String) -> String {
+ companion object {
+ @JvmStatic
+ val filters: MutableList = mutableListOf()
+
+ val maskedFields: MutableSet = mutableSetOf()
+
+ @JvmStatic
+ val INSTANCE = MaskLogs()
+
+ /**
+ * Executes the masking process on the provided log string.
+ *
+ * @param logs The log string that may contain sensitive information requiring masking.
+ */
+ @JvmStatic
+ fun execute(logs: String) = INSTANCE(logs)
+
+ /**
+ * Adds specified fields to the list of fields to be masked in logs.
+ *
+ * The fields provided in the parameter are added to the internal set of fields
+ * and corresponding filters are created and added to the filter list. These filters
+ * are then integrated into the masking system to ensure the specified fields are
+ * masked in any logs they appear in.
+ *
+ * @param fields The set of field names that need to be masked in the logs.
+ */
+ @JvmStatic
+ fun addFields(fields: Set) {
+ maskedFields.addAll(fields)
+ filters.add(JsonFieldFilter(fields.toTypedArray()))
+ filters.forEach { EJMaskInitializer.addFilter(it) }
+ }
+ }
+
+ init {
+ LoggerUtil.register { _, _, _ -> /* disable logging */ }
+ }
+
+ /**
+ * Masks the given text using the EJMask utility.
+ *
+ * @param text The input text that needs to be masked.
+ * @return The masked version of the input text.
+ */
+ override fun invoke(text: String): String {
+ return EJMask.mask(text)
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Headers.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/Headers.kt
deleted file mode 100644
index 47c14c2e..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Headers.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2022 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.model
-
-import com.expediagroup.sdk.core.constant.HeaderKey
-import io.ktor.http.Headers
-import io.ktor.http.HeadersBuilder
-
-internal fun Headers.getTransactionId(): String? = get(HeaderKey.TRANSACTION_ID)
-
-internal fun HeadersBuilder.getTransactionId(): String? = get(HeaderKey.TRANSACTION_ID)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Nothing.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/Nothing.kt
deleted file mode 100644
index bd6863c4..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Nothing.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2022 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.model
-
-/**
- * A representation of nothingness. Philosophers have debated the existence of nothing for centuries, but we have finally found it.
- */
-data object Nothing
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Properties.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/Properties.kt
deleted file mode 100644
index 5c93b999..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Properties.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2022 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.model
-
-import java.io.BufferedReader
-import java.io.InputStreamReader
-import java.net.URL
-
-/** A model of "*.properties" file with some handy methods. */
-class Properties(private val data: Map) {
- companion object {
- /** Creates a new SdkProperties with the given data. */
- fun from(path: URL) =
- Properties(
- java.util.Properties().apply {
- load(BufferedReader(InputStreamReader(path.openStream())))
- }.map { it.key.toString() to it.value.toString() }.toMap()
- )
- }
-
- /** Returns the data for a given [key]. */
- operator fun get(key: String): String? = data[key]
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Response.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/Response.kt
deleted file mode 100644
index daceba63..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/Response.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-@file:Suppress("unused")
-
-package com.expediagroup.sdk.core.model
-
-import java.util.stream.Collectors
-import kotlin.collections.Map.Entry
-
-/**
- * A Generic Response to represent the response from a service call.
- *
- * @property statusCode The HTTP status code of the response
- * @property body The body of the response
- * @property headers The headers of the response
- */
-@Suppress("MemberVisibilityCanBePrivate")
-open class Response(
- val statusCode: Int,
- val body: T,
- val headers: Map>
-) {
- constructor(statusCode: Int, body: T, headers: Set>>) : this(statusCode, body, toHeadersMap(headers))
-
- companion object {
- @JvmStatic
- fun toHeadersMap(headers: Set>>): Map> =
- headers.stream().collect(
- Collectors.toMap(
- Entry>::key,
- Entry>::value
- )
- )
- }
-
- override fun toString(): String = "Response(statusCode=$statusCode, body=$body, headers=$headers)"
-}
-
-class EmptyResponse(
- statusCode: Int,
- headers: Map>
-) : Response(statusCode, Nothing, headers) {
- constructor(statusCode: Int, headers: Set>>) : this(statusCode, toHeadersMap(headers))
-
- override fun toString(): String = "EmptyResponse(statusCode=$statusCode, headers=$headers)"
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/ExceptionUtils.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/ExceptionUtils.kt
deleted file mode 100644
index 02fcaabd..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/ExceptionUtils.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2022 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.model.exception
-
-import com.expediagroup.sdk.core.constant.provider.ExceptionMessageProvider
-import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupServiceException
-
-/** Handles exceptions by ensuring that only instances of [ExpediaGroupException] are thrown. */
-fun Throwable.handle(): Nothing = handleWith(null)
-
-/**
- * Handles exceptions by ensuring that only instances of [ExpediaGroupException] are thrown.
- *
- * @param transactionId the transaction ID to be included in the exception message, can be null.
- */
-fun Throwable.handleWith(transactionId: String?): Nothing {
- if (this is ExpediaGroupException) throw this
-
- when (val cause = this.cause) {
- is ExpediaGroupException -> throw cause
- else -> throw ExpediaGroupServiceException(
- ExceptionMessageProvider.getExceptionOccurredWithTransactionIdMessage(transactionId, message),
- this,
- transactionId
- )
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/client/ExpediaGroupInvalidFieldNameException.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/client/ExpediaGroupResponseParsingException.kt
similarity index 55%
rename from code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/client/ExpediaGroupInvalidFieldNameException.kt
rename to code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/client/ExpediaGroupResponseParsingException.kt
index 6d06ffc8..e8c1cbfc 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/client/ExpediaGroupInvalidFieldNameException.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/client/ExpediaGroupResponseParsingException.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 Expedia, Inc.
+ * 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.
@@ -13,12 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.expediagroup.sdk.core.model.exception.client
/**
- * Thrown to indicate that one or more passed field names are invalid.
+ * Exception thrown when the SDK fails to parse a service response.
+ *
+ * This is a client-side exception that indicates the response was received
+ * but could not be properly deserialized into the expected format.
*
- * @param invalidFields the names of the invalid fields.
+ * @param message A description of the parsing failure
+ * @param cause The underlying parsing/mapping exception
*/
-class ExpediaGroupInvalidFieldNameException(invalidFields: Collection) :
- ExpediaGroupClientException("All fields names must contain only alphanumeric characters in addition to - and _ but found [${invalidFields.joinToString(",")}]")
+class ExpediaGroupResponseParsingException(
+ message: String? = null,
+ cause: Throwable? = null,
+) : ExpediaGroupClientException(message, cause)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupApiException.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupApiException.kt
deleted file mode 100644
index 64e901c5..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupApiException.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2022 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.model.exception.service
-
-import com.expediagroup.sdk.core.constant.provider.LoggingMessageProvider.getTransactionIdMessage
-
-abstract class ExpediaGroupApiException(val statusCode: Int, open val errorObject: Any, transactionId: String?) :
- ExpediaGroupServiceException("Unsuccessful response code [$statusCode]${getTransactionIdMessage(transactionId)}${stringifyErrorObject(errorObject.toString())}", transactionId = transactionId)
-
-private fun stringifyErrorObject(stringValue: String): String = if (stringValue.isBlank()) " with an empty response body" else ": $stringValue"
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupAuthException.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupAuthException.kt
index d3d6ac14..63de302c 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupAuthException.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupAuthException.kt
@@ -15,7 +15,7 @@
*/
package com.expediagroup.sdk.core.model.exception.service
-import io.ktor.http.HttpStatusCode
+import com.expediagroup.sdk.core.http.Status
/**
* An exception that is thrown when an authentication error occurs.
@@ -32,12 +32,22 @@ class ExpediaGroupAuthException(
/**
* An exception that is thrown when an authentication error occurs.
*
- * @param errorCode The HTTP status code of the error.
+ * @param status The HTTP status of the error.
* @param message The error message.
*/
constructor(
- errorCode: HttpStatusCode,
+ status: Status,
message: String,
- transactionId: String?
- ) : this(message = "[${errorCode.value}] $message", transactionId = transactionId)
+ ) : this(message = "[${status.code}] $message")
+
+ /**
+ * An exception that is thrown when an authentication error occurs.
+ *
+ * @param status The HTTP status of the error (as an integer).
+ * @param message The error message.
+ */
+ constructor(
+ status: Int,
+ message: String,
+ ) : this(Status.fromCode(status), message)
}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupNetworkException.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupNetworkException.kt
new file mode 100644
index 00000000..8548ba28
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupNetworkException.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.model.exception.service
+
+/**
+ * Exception thrown when network-related errors occur during service operations.
+ *
+ * This exception wraps network-level failures that occur while communicating with
+ * Expedia Group services (e.g., connection timeouts, DNS failures, SSL/TLS errors).
+ *
+ * @param message A human-readable description of the network error
+ * @param cause The underlying exception that caused this network error
+ * @param transactionId Unique identifier for tracking this request across systems
+ */
+class ExpediaGroupNetworkException(
+ message: String? = null,
+ cause: Throwable? = null,
+ transactionId: String? = null
+) : ExpediaGroupServiceException(message, cause, transactionId)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupServiceDefaultErrorException.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupServiceDefaultErrorException.kt
deleted file mode 100644
index e25db920..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/exception/service/ExpediaGroupServiceDefaultErrorException.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2022 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.model.exception.service
-
-class ExpediaGroupServiceDefaultErrorException(code: Int, override val errorObject: String, transactionId: String?) : ExpediaGroupApiException(code, errorObject, transactionId)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/paging/Paginator.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/paging/Paginator.kt
deleted file mode 100644
index 51666c17..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/paging/Paginator.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2022 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.model.paging
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.constant.HeaderKey.LINK
-import com.expediagroup.sdk.core.constant.HeaderKey.PAGINATION_TOTAL_RESULTS
-import com.expediagroup.sdk.core.model.Response
-import io.ktor.client.statement.HttpResponse
-
-sealed class BasePaginator(
- private val client: Client,
- firstResponse: Response,
- private val fallbackBody: T,
- private val getBody: suspend (HttpResponse) -> T
-) : Iterator {
- private var state: ResponseState = DefaultResponseState(firstResponse)
- val paginationTotalResults: Long = firstResponse.headers[PAGINATION_TOTAL_RESULTS]?.getOrNull(0)?.toLongOrNull() ?: 0
-
- override fun hasNext(): Boolean = state.hasNext()
-
- private fun extractLink(headers: Map>): String? {
- return headers[LINK]?.getOrNull(0)?.split(";")?.let {
- if (it.isNotEmpty()) it[0] else null
- }?.let {
- it.substring(it.indexOf("<") + 1, it.indexOf(">"))
- }
- }
-
- protected fun nextResponse(): Response {
- val response = state.getNextResponse()
- state = ResponseStateFactory.getState(extractLink(response.headers), client, fallbackBody, getBody)
- return response
- }
-}
-
-/**
- * Paginator that returns the body of the response.
- *
- * @param client The client to use to fetch the next response
- * @param firstResponse The first response to start the paginator with
- * @param getBody A function to extract the body from the response
- */
-class Paginator(
- client: Client,
- firstResponse: Response,
- fallbackBody: T,
- getBody: suspend (HttpResponse) -> T
-) : BasePaginator(client, firstResponse, fallbackBody, getBody) {
- /**
- * Returns the body of the next response.
- *
- * @throws NoSuchElementException if the iteration has no next element.
- */
- override fun next(): T = nextResponse().body
-}
-
-/**
- * Paginator that returns the full response.
- *
- * @param client The client to use to fetch the next response
- * @param firstResponse The first response to start the paginator with
- * @param getBody A function to extract the body from the response
- */
-class ResponsePaginator(
- client: Client,
- firstResponse: Response,
- fallbackBody: T,
- getBody: suspend (HttpResponse) -> T
-) : BasePaginator, T>(client, firstResponse, fallbackBody, getBody) {
- /**
- * Returns the next response.
- *
- * @throws NoSuchElementException if the iteration has no next element.
- */
- override fun next(): Response = nextResponse()
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/model/paging/ResponseState.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/model/paging/ResponseState.kt
deleted file mode 100644
index acaf8513..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/model/paging/ResponseState.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2022 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.model.paging
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.constant.HeaderValue
-import com.expediagroup.sdk.core.model.Response
-import com.expediagroup.sdk.core.plugin.logging.GZipEncoder.decode
-import com.expediagroup.sdk.core.plugin.logging.contentEncoding
-import io.ktor.client.statement.HttpResponse
-import io.ktor.util.InternalAPI
-import io.ktor.util.moveToByteArray
-import io.ktor.utils.io.ByteReadChannel
-import io.ktor.utils.io.bits.Memory
-import kotlinx.coroutines.runBlocking
-import java.nio.ByteBuffer
-
-internal interface ResponseState {
- fun getNextResponse(): Response
-
- fun hasNext(): Boolean
-}
-
-internal class DefaultResponseState(
- private val response: Response
-) : ResponseState {
- override fun getNextResponse(): Response {
- return response
- }
-
- override fun hasNext(): Boolean {
- return true
- }
-}
-
-internal class LastResponseState : ResponseState {
- override fun getNextResponse(): Response {
- throw NoSuchElementException()
- }
-
- override fun hasNext(): Boolean {
- return false
- }
-}
-
-internal class FetchLinkState(
- private val link: String,
- private val client: Client,
- private val fallbackBody: T,
- private val getBody: suspend (HttpResponse) -> T
-) : ResponseState {
- override fun getNextResponse(): Response {
- return runBlocking {
- val response = client.performGet(link)
- val body = parseBody(response)
- Response(response.status.value, body, response.headers.entries())
- }
- }
-
- override fun hasNext(): Boolean {
- return true
- }
-
- private suspend fun parseBody(response: HttpResponse): T {
- return if (decodeBody(response).isEmpty()) fallbackBody else getBody(response)
- }
-
- private suspend fun decodeBody(response: HttpResponse): String {
- val byteReadChannel = prepareByteReadChannel(response)
- val decodedByteReadChannel = if (response.contentEncoding().equals(HeaderValue.GZIP)) client.httpClient.decode(byteReadChannel) else byteReadChannel
- val bodyString: String = decodedByteReadChannel.readRemaining().readText()
- return bodyString
- }
-
- @OptIn(InternalAPI::class)
- private suspend fun prepareByteReadChannel(response: HttpResponse): ByteReadChannel {
- val bufferSize = response.content.availableForRead
- val buffer = ByteBuffer.allocate(bufferSize)
- val numberOfBytesRead = response.content.peekTo(Memory(buffer), 0, 0, 0, bufferSize.toLong()).toInt()
- val byteReadChannel = ByteReadChannel(buffer.moveToByteArray(), 0, numberOfBytesRead)
- return byteReadChannel
- }
-}
-
-internal class ResponseStateFactory {
- companion object {
- fun getState(
- link: String?,
- client: Client,
- fallbackBody: T,
- getBody: suspend (HttpResponse) -> T
- ): ResponseState {
- return link?.let { FetchLinkState(it, client, fallbackBody, getBody) } ?: LastResponseState()
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/BaseOkHttpClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/BaseOkHttpClient.kt
new file mode 100644
index 00000000..40cf81a4
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/BaseOkHttpClient.kt
@@ -0,0 +1,97 @@
+/*
+ * 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 java.time.Duration
+import okhttp3.OkHttpClient
+
+/**
+ * A utility object for managing and configuring a singleton instance of an `OkHttpClient`.
+ *
+ * The `BaseOkHttpClient` object provides methods to retrieve a singleton instance of
+ * `OkHttpClient` and to configure a new instance based on the provided `OkHttpClientConfiguration`.
+ * It ensures thread safety and avoids recreating the client unnecessarily.
+ *
+ * ## Usage
+ * - Use `getInstance()` to retrieve the singleton instance of `OkHttpClient`.
+ * - Use `getConfiguredInstance(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
+
+ /**
+ * Retrieves the singleton instance of `OkHttpClient`.
+ *
+ * This method ensures that the instance is initialized lazily and safely for concurrent access.
+ * If the instance is not yet initialized, it will create a new instance.
+ *
+ * @return The singleton instance of `OkHttpClient`.
+ */
+ fun getInstance(): OkHttpClient {
+ return instance ?: synchronized(this) {
+ instance ?: OkHttpClient().also { instance = it }
+ }
+ }
+
+ /**
+ * 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`.
+ *
+ * @param configuration The `OkHttpClientConfiguration` containing settings for the client.
+ * @return A new `OkHttpClient` instance configured with the specified settings.
+ */
+ fun getConfiguredInstance(configuration: OkHttpClientConfiguration): OkHttpClient = getInstance()
+ .newBuilder()
+ .apply {
+ configuration.callTimeout?.let {
+ callTimeout(Duration.ofMillis(it.toLong()))
+ }
+ configuration.connectTimeout?.let {
+ connectTimeout(Duration.ofMillis(it.toLong()))
+ }
+ configuration.readTimeout?.let {
+ readTimeout(Duration.ofMillis(it.toLong()))
+ }
+ configuration.writeTimeout?.let {
+ writeTimeout(Duration.ofMillis(it.toLong()))
+ }
+ configuration.connectionPool?.let {
+ connectionPool(it)
+ }
+ configuration.retryOnConnectionFailure?.let {
+ retryOnConnectionFailure(it)
+ }
+ configuration.interceptors?.forEach {
+ addInterceptor(it)
+ }
+ configuration.networkInterceptors?.forEach {
+ addNetworkInterceptor(it)
+ }
+ }.build()
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/OkHttpClientConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/OkHttpClientConfiguration.kt
new file mode 100644
index 00000000..136b2888
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/OkHttpClientConfiguration.kt
@@ -0,0 +1,191 @@
+/*
+ * 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 okhttp3.ConnectionPool
+import okhttp3.Interceptor
+
+/**
+ * Represents configuration options for an `OkHttpClient` instance.
+ *
+ * The `OkHttpClientConfiguration` class encapsulates various settings that can be applied
+ * to an `OkHttpClient`, including interceptors, timeouts, connection pooling, and retry behavior.
+ * It provides a nested `Builder` class for constructing instances in a flexible and fluent manner.
+ *
+ * ## Configuration Options
+ * - **Interceptors**: Application-level interceptors for modifying requests or responses.
+ * - **Network Interceptors**: Network-level interceptors for observing network traffic.
+ * - **Connection Pool**: Customizes the connection pooling behavior.
+ * - **Retry on Connection Failure**: Specifies whether the client should retry failed connections.
+ * - **Timeouts**: Configures call, connect, read, and write timeouts.
+ *
+ * ## Example Usage
+ * ```kotlin
+ * val configuration = OkHttpClientConfiguration.builder()
+ * .interceptors(listOf(loggingInterceptor))
+ * .callTimeout(30_000) // 30 seconds
+ * .connectTimeout(10_000) // 10 seconds
+ * .retryOnConnectionFailure(true)
+ * .build()
+ * ```
+ *
+ * @property interceptors A list of application-level interceptors to apply.
+ * @property networkInterceptors A list of network-level interceptors to apply.
+ * @property connectionPool The connection pool configuration.
+ * @property retryOnConnectionFailure Whether to retry on connection failure.
+ * @property callTimeout The timeout for a complete HTTP call, in milliseconds.
+ * @property connectTimeout The timeout for establishing a connection, in milliseconds.
+ * @property readTimeout The timeout for reading data from a connection, in milliseconds.
+ * @property writeTimeout The timeout for writing data to a connection, in milliseconds.
+ */
+data class OkHttpClientConfiguration(
+ val interceptors: List? = null,
+ val networkInterceptors: List? = null,
+ val connectionPool: ConnectionPool? = null,
+ val retryOnConnectionFailure: Boolean? = null,
+ val callTimeout: Int? = null,
+ val connectTimeout: Int? = null,
+ val readTimeout: Int? = null,
+ val writeTimeout: Int? = null,
+) {
+
+ /**
+ * A builder class for constructing `OkHttpClientConfiguration` instances.
+ *
+ * The `Builder` class provides a fluent API for setting configuration options
+ * and creating an instance of `OkHttpClientConfiguration`.
+ *
+ * ## Example Usage
+ * ```kotlin
+ * val configuration = OkHttpClientConfiguration.builder()
+ * .callTimeout(15_000)
+ * .readTimeout(20_000)
+ * .retryOnConnectionFailure(true)
+ * .build()
+ * ```
+ */
+ open class Builder {
+ private var interceptors: List? = null
+ private var networkInterceptors: List? = null
+ private var connectionPool: ConnectionPool? = null
+ private var retryOnConnectionFailure: Boolean? = null
+ private var callTimeout: Int? = null
+ private var connectTimeout: Int? = null
+ private var readTimeout: Int? = null
+ private var writeTimeout: Int? = null
+
+ /**
+ * Sets the application-level interceptors.
+ * @param interceptors A list of interceptors to apply.
+ * @return The builder instance.
+ */
+ fun interceptors(interceptors: List) = apply {
+ this.interceptors = interceptors
+ }
+
+ /**
+ * Sets the network-level interceptors.
+ * @param networkInterceptors A list of interceptors to apply.
+ * @return The builder instance.
+ */
+ fun networkInterceptors(networkInterceptors: List) = apply {
+ this.networkInterceptors = networkInterceptors
+ }
+
+ /**
+ * Sets the connection pool configuration.
+ * @param connectionPool The connection pool to use.
+ * @return The builder instance.
+ */
+ fun connectionPool(connectionPool: ConnectionPool) = apply {
+ this.connectionPool = connectionPool
+ }
+
+ /**
+ * Sets whether to retry on connection failure.
+ * @param retryOnConnectionFailure `true` to retry on failure, `false` otherwise.
+ * @return The builder instance.
+ */
+ fun retryOnConnectionFailure(retryOnConnectionFailure: Boolean) = apply {
+ this.retryOnConnectionFailure = retryOnConnectionFailure
+ }
+
+ /**
+ * Sets the call timeout.
+ * @param callTimeout The timeout duration in milliseconds.
+ * @return The builder instance.
+ */
+ fun callTimeout(callTimeout: Int) = apply {
+ this.callTimeout = callTimeout
+ }
+
+ /**
+ * Sets the connection timeout.
+ * @param connectTimeout The timeout duration in milliseconds.
+ * @return The builder instance.
+ */
+ fun connectTimeout(connectTimeout: Int) = apply {
+ this.connectTimeout = connectTimeout
+ }
+
+ /**
+ * Sets the read timeout.
+ * @param readTimeout The timeout duration in milliseconds.
+ * @return The builder instance.
+ */
+ fun readTimeout(readTimeout: Int) = apply {
+ this.readTimeout = readTimeout
+ }
+
+ /**
+ * Sets the write timeout.
+ * @param writeTimeout The timeout duration in milliseconds.
+ * @return The builder instance.
+ */
+ fun writeTimeout(writeTimeout: Int) = apply {
+ this.writeTimeout = writeTimeout
+ }
+
+ /**
+ * Builds and returns an `OkHttpClientConfiguration` instance.
+ *
+ * @return A configured instance of `OkHttpClientConfiguration`.
+ */
+ fun build(): OkHttpClientConfiguration {
+ return OkHttpClientConfiguration(
+ interceptors,
+ networkInterceptors,
+ connectionPool,
+ retryOnConnectionFailure,
+ callTimeout,
+ connectTimeout,
+ readTimeout,
+ writeTimeout
+ )
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a new builder for constructing an `OkHttpClientConfiguration`.
+ *
+ * @return A new instance of `Builder`.
+ */
+ @JvmStatic
+ fun builder() = Builder()
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/OkHttpTransport.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/OkHttpTransport.kt
new file mode 100644
index 00000000..92694379
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/core/okhttp/OkHttpTransport.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.client.Transport
+import com.expediagroup.sdk.core.http.MediaType.Companion.parse
+import com.expediagroup.sdk.core.http.Protocol
+import com.expediagroup.sdk.core.http.ResponseBody.Companion.create
+import java.io.IOException
+import okhttp3.Headers
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.Response
+import okhttp3.ResponseBody
+import okio.BufferedSink
+
+typealias OkHttpRequest = Request
+typealias OkHttpRequestBuilder = Request.Builder
+typealias OkHttpRequestBody = RequestBody
+typealias OkHttpResponse = Response
+typealias OkHttpResponseBody = ResponseBody
+typealias OkHttpHeaders = Headers
+typealias OkHttpHeadersBuilder = Headers.Builder
+
+typealias SdkRequest = com.expediagroup.sdk.core.http.Request
+typealias SdkRequestBody = com.expediagroup.sdk.core.http.RequestBody
+typealias SdkResponse = com.expediagroup.sdk.core.http.Response
+typealias SdkResponseBuilder = com.expediagroup.sdk.core.http.Response.Builder
+typealias SdkHeaders = com.expediagroup.sdk.core.http.Headers
+typealias SdkHeadersBuilder = com.expediagroup.sdk.core.http.Headers.Builder
+
+class OkHttpTransport(
+ private val okHttpClient: OkHttpClient
+) : Transport {
+
+ @Throws(IOException::class)
+ override fun execute(request: SdkRequest): SdkResponse {
+ val okHttpRequest = toOkHttpRequest(request)
+ val okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()
+ return toSdkHttpResponse(okHttpResponse, request)
+ }
+
+ private fun toOkHttpRequest(sdkRequest: SdkRequest): OkHttpRequest {
+ return OkHttpRequestBuilder().apply {
+ url(sdkRequest.url.toUrl())
+ headers(buildHeaders(sdkRequest.headers))
+ method(sdkRequest.method, createRequestBody(sdkRequest.body))
+ tag(sdkRequest.tags)
+ }.build()
+ }
+
+ private fun buildHeaders(headers: SdkHeaders): OkHttpHeaders {
+ return OkHttpHeadersBuilder().apply {
+ headers.entries().forEach { (name, values) ->
+ values.forEach { value -> add(name, value) }
+ }
+ }.build()
+ }
+
+ private fun createRequestBody(body: SdkRequestBody?): OkHttpRequestBody? {
+ return body?.let {
+ object : OkHttpRequestBody() {
+ override fun contentType() = it.mediaType()?.toString()?.toMediaTypeOrNull()
+ override fun contentLength() = it.contentLength()
+
+ @Throws(IOException::class)
+ override fun writeTo(sink: BufferedSink) = it.writeTo(sink)
+ }
+ }
+ }
+
+ private fun toSdkHttpResponse(okHttpResponse: OkHttpResponse, sdkRequest: SdkRequest): SdkResponse {
+ return SdkResponseBuilder().apply {
+ headers(buildSdkHeaders(okHttpResponse.headers))
+ okHttpResponse.body?.let { buildResponseBody(it) }?.let { body(it) }
+ request(sdkRequest)
+ protocol(Protocol.valueOf(okHttpResponse.protocol.name))
+ code(okHttpResponse.code)
+ message(okHttpResponse.message)
+ }.build()
+ }
+
+ private fun buildSdkHeaders(headers: OkHttpHeaders): SdkHeaders {
+ return SdkHeadersBuilder().apply {
+ headers.toMultimap().forEach { (key, values) -> add(key, values) }
+ }.build()
+ }
+
+ private fun buildResponseBody(body: OkHttpResponseBody) = body.run {
+ create(
+ source = source(),
+ contentLength = contentLength(),
+ mediaType = contentType()?.toString()?.let(::parse)
+ )
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/Hook.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/Hook.kt
deleted file mode 100644
index 9a688c6b..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/Hook.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin
-
-import com.expediagroup.sdk.core.client.Client
-
-/**
- * A helper to build a hook.
- */
-internal interface HookBuilder {
- fun build(configs: C)
-}
-
-/**
- * A hook is a post action we need to apply after creating a [Client].
- */
-internal open class Hook(
- private val configuration: C,
- private val builder: HookBuilder
-) {
- fun execute() = builder.build(configuration)
-}
-
-/** A singleton repository of all [Hook]s we need to apply on the [Client]. */
-internal object Hooks {
- private val clientsHooks: MutableMap>> = mutableMapOf()
-
- fun add(
- client: Client,
- hook: Hook
- ) {
- clientsHooks.getOrPut(client) { mutableListOf() } += hook
- }
-
- fun execute(client: Client) {
- clientsHooks[client]?.forEach { it.execute() }
- }
-}
-
-/**
- * Provide an idiomatic scope to define hooks.
- */
-internal fun Client.hooks(block: HookContext.() -> Unit) = block(HookContext(this))
-
-internal class HookContext(private val client: Client) {
- /**
- * Provides an idiomatic way of defining a hook.
- */
- fun use(hookFactory: HookFactory) = hookFactory
-
- /**
- * Provides an idiomatic way of configuring a hook.
- */
- fun HookFactory.with(config: C) = Hooks.add(client, create(client, config))
-}
-
-internal interface HookFactory {
- fun create(
- client: Client,
- configuration: C
- ): Hook
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/Plugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/Plugin.kt
deleted file mode 100644
index 6a81db9d..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/Plugin.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin
-
-import com.expediagroup.sdk.core.client.Client
-
-internal interface Plugin {
- /** Install a plugin. */
- fun install(
- client: Client,
- configurations: C
- )
-}
-
-/**
- * Provide an idiomatic scope to define plugins.
- */
-internal fun Client.plugins(block: PluginContext.() -> Unit) = block(PluginContext(this))
-
-internal class PluginContext(private val client: Client) {
- /**
- * Provides an idiomatic way of starting a plugin.
- */
- fun > use(plugin: P): P = plugin
-
- /**
- * Provides an idiomatic way of configuring a plugin.
- */
- fun Plugin.with(configs: C) = install(client, configs)
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/PluginConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/PluginConfiguration.kt
deleted file mode 100644
index 8eafdd38..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/PluginConfiguration.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin
-
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-
-internal interface PluginConfiguration
-
-internal abstract class KtorPluginConfiguration(open val httpClientConfiguration: HttpClientConfig) : PluginConfiguration
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationConfiguration.kt
deleted file mode 100644
index 4e0a69c2..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationConfiguration.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.authentication
-
-import com.expediagroup.sdk.core.configuration.Credentials
-import com.expediagroup.sdk.core.plugin.KtorPluginConfiguration
-import com.expediagroup.sdk.core.plugin.authentication.strategy.AuthenticationStrategy.AuthenticationType
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-
-internal data class AuthenticationConfiguration(
- override val httpClientConfiguration: HttpClientConfig,
- val credentials: Credentials,
- val authUrl: String,
- val authType: AuthenticationType
-) : KtorPluginConfiguration(httpClientConfiguration) {
- companion object {
- fun from(
- httpClientConfig: HttpClientConfig,
- credentials: Credentials,
- authUrl: String,
- authType: AuthenticationType = AuthenticationType.BEARER
- ) = AuthenticationConfiguration(httpClientConfig, credentials, authUrl, authType)
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationHookFactory.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationHookFactory.kt
deleted file mode 100644
index e919f21a..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationHookFactory.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.authentication
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.constant.Authentication.AUTHORIZATION_REQUEST_LOCK_DELAY
-import com.expediagroup.sdk.core.plugin.Hook
-import com.expediagroup.sdk.core.plugin.HookBuilder
-import com.expediagroup.sdk.core.plugin.HookFactory
-import io.ktor.client.plugins.HttpSend
-import io.ktor.client.plugins.plugin
-import io.ktor.client.request.HttpRequestBuilder
-import io.ktor.http.HttpHeaders
-import kotlinx.atomicfu.atomic
-import kotlinx.coroutines.delay
-
-internal object AuthenticationHookFactory : HookFactory {
- override fun create(
- client: Client,
- configuration: AuthenticationConfiguration
- ): Hook {
- return Hook(configuration, AuthenticationHookBuilder(client))
- }
-}
-
-private class AuthenticationHookBuilder(private val client: Client) : HookBuilder {
- private val lock = atomic(false)
- private val authenticationStrategy = client.getAuthenticationStrategy()
-
- override fun build(configs: AuthenticationConfiguration) {
- val httpClient = client.httpClient
-
- httpClient.plugin(HttpSend).intercept { request ->
- if (!authenticationStrategy.isIdentityRequest(request)) {
- if (authenticationStrategy.isTokenAboutToExpire()) {
- if (!lock.getAndSet(true)) {
- try {
- authenticationStrategy.renewToken()
- } finally {
- lock.compareAndSet(expect = true, update = false)
- }
- }
- }
- while (lock.value) delay(AUTHORIZATION_REQUEST_LOCK_DELAY)
- assignLatestToken(request)
- }
- execute(request)
- }
- }
-
- private fun assignLatestToken(request: HttpRequestBuilder) {
- request.headers[HttpHeaders.Authorization] = authenticationStrategy.getAuthorizationHeader()
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationPlugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationPlugin.kt
deleted file mode 100644
index 174f9fee..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/AuthenticationPlugin.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.authentication
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.constant.ExceptionMessage.AUTHENTICATION_NOT_CONFIGURED_FOR_CLIENT
-import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupClientException
-import com.expediagroup.sdk.core.plugin.Plugin
-import com.expediagroup.sdk.core.plugin.authentication.strategy.AuthenticationStrategy
-import io.ktor.client.plugins.auth.Auth
-import kotlin.collections.set
-
-internal object AuthenticationPlugin : Plugin {
- val clientAuthenticationStrategies = mutableMapOf()
-
- override fun install(
- client: Client,
- configurations: AuthenticationConfiguration
- ) {
- val strategy = AuthenticationStrategy.from(configurations, client)
- clientAuthenticationStrategies[client] = strategy
- configurations.httpClientConfiguration.install(Auth) {
- strategy.loadAuth(this)
- }
- }
-}
-
-internal fun Client.getAuthenticationStrategy(): AuthenticationStrategy =
- AuthenticationPlugin.clientAuthenticationStrategies[this] ?: throw ExpediaGroupClientException(AUTHENTICATION_NOT_CONFIGURED_FOR_CLIENT)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/AuthenticationStrategy.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/AuthenticationStrategy.kt
deleted file mode 100644
index 34ee3000..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/AuthenticationStrategy.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.authentication.strategy
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.plugin.authentication.AuthenticationConfiguration
-import com.expediagroup.sdk.core.plugin.authentication.strategy.AuthenticationStrategy.AuthenticationType.BEARER
-import com.expediagroup.sdk.core.plugin.authentication.strategy.AuthenticationStrategy.AuthenticationType.SIGNATURE
-import io.ktor.client.plugins.auth.Auth
-import io.ktor.client.request.HttpRequestBuilder
-
-internal interface AuthenticationStrategy {
- fun loadAuth(auth: Auth) {}
-
- fun isTokenAboutToExpire(): Boolean
-
- fun renewToken()
-
- fun isIdentityRequest(request: HttpRequestBuilder): Boolean
-
- fun getAuthorizationHeader(): String
-
- companion object {
- fun from(
- configs: AuthenticationConfiguration,
- client: Client
- ): AuthenticationStrategy =
- when (configs.authType) {
- BEARER -> ExpediaGroupAuthenticationStrategy(client, configs)
- SIGNATURE -> RapidAuthenticationStrategy(configs)
- }
- }
-
- enum class AuthenticationType {
- BEARER,
- SIGNATURE
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/ExpediaGroupAuthenticationStrategy.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/ExpediaGroupAuthenticationStrategy.kt
deleted file mode 100644
index fdd80af0..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/ExpediaGroupAuthenticationStrategy.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.authentication.strategy
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.configuration.Credentials
-import com.expediagroup.sdk.core.constant.Authentication
-import com.expediagroup.sdk.core.constant.Constant
-import com.expediagroup.sdk.core.constant.ExceptionMessage
-import com.expediagroup.sdk.core.constant.LoggingMessage
-import com.expediagroup.sdk.core.constant.provider.LoggingMessageProvider
-import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException
-import com.expediagroup.sdk.core.model.getTransactionId
-import com.expediagroup.sdk.core.plugin.authentication.AuthenticationConfiguration
-import com.expediagroup.sdk.core.plugin.logging.ExpediaGroupLoggerFactory
-import com.fasterxml.jackson.annotation.JsonCreator
-import com.fasterxml.jackson.annotation.JsonProperty
-import com.fasterxml.jackson.databind.ObjectMapper
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.auth.Auth
-import io.ktor.client.plugins.auth.providers.BearerAuthProvider
-import io.ktor.client.plugins.auth.providers.BearerTokens
-import io.ktor.client.plugins.auth.providers.bearer
-import io.ktor.client.plugins.plugin
-import io.ktor.client.request.HttpRequestBuilder
-import io.ktor.client.request.basicAuth
-import io.ktor.client.request.parameter
-import io.ktor.client.request.request
-import io.ktor.client.request.url
-import io.ktor.client.statement.bodyAsText
-import io.ktor.http.ContentType
-import io.ktor.http.HttpMethod
-import io.ktor.http.ParametersBuilder
-import io.ktor.http.clone
-import io.ktor.http.contentType
-import kotlinx.coroutines.runBlocking
-import java.time.LocalDateTime
-
-internal class ExpediaGroupAuthenticationStrategy(
- private val client: Client,
- private val configs: AuthenticationConfiguration,
-) : AuthenticationStrategy {
- private val log = ExpediaGroupLoggerFactory.getLogger(javaClass)
- private var bearerTokenStorage = BearerTokensInfo.emptyBearerTokenInfo
- private val objectMapper = ObjectMapper()
-
- override fun loadAuth(auth: Auth) {
- auth.bearer {
- sendWithoutRequest { request ->
- isIdentityRequest(request)
- }
- }
- }
-
- override fun isTokenAboutToExpire(): Boolean =
- bearerTokenStorage.isAboutToExpire().also { if (it) log.info(LoggingMessage.TOKEN_EXPIRED) }
-
- override fun renewToken() {
- val httpClient = client.httpClient
- log.info(LoggingMessage.TOKEN_RENEWAL_IN_PROGRESS)
- clearTokens(httpClient)
- val renewTokenResponse =
- runBlocking {
- httpClient.request {
- method = HttpMethod.Post
- parameter(Authentication.GRANT_TYPE, Authentication.CLIENT_CREDENTIALS)
- contentType(ContentType.Application.FormUrlEncoded)
- url(configs.authUrl)
- basicAuth(configs.credentials)
- with(client) { appendHeaders() }
- }
- }
- if (renewTokenResponse.status.value !in Constant.SUCCESSFUL_STATUS_CODES_RANGE) {
- throw ExpediaGroupAuthException(
- renewTokenResponse.status,
- ExceptionMessage.AUTHENTICATION_FAILURE,
- renewTokenResponse.headers.getTransactionId(),
- )
- }
- val renewedTokenInfo: TokenResponse =
- runBlocking {
- objectMapper.readValue(renewTokenResponse.bodyAsText(), TokenResponse::class.java)
- }
- log.info(LoggingMessage.TOKEN_RENEWAL_SUCCESSFUL)
- log.info(LoggingMessageProvider.getTokenExpiresInMessage(renewedTokenInfo.expiresIn))
- bearerTokenStorage =
- BearerTokensInfo(
- BearerTokens(renewedTokenInfo.accessToken, renewedTokenInfo.accessToken),
- renewedTokenInfo.expiresIn,
- )
- bearerTokenStorage
- }
-
- private fun clearTokens(client: HttpClient) {
- log.info(LoggingMessage.TOKEN_CLEARING_IN_PROGRESS)
- client.plugin(Auth).providers.filterIsInstance().first().clearToken()
- bearerTokenStorage = BearerTokensInfo.emptyBearerTokenInfo
- log.info(LoggingMessage.TOKEN_CLEARING_SUCCESSFUL)
- }
-
- private fun getTokens(): BearerTokens = bearerTokenStorage.bearerTokens
-
- private fun HttpRequestBuilder.basicAuth(credentials: Credentials) {
- basicAuth(
- credentials.key,
- credentials.secret,
- )
- }
-
- override fun isIdentityRequest(request: HttpRequestBuilder): Boolean =
- request.url.clone().apply {
- encodedParameters = ParametersBuilder()
- }.buildString() == configs.authUrl
-
- override fun getAuthorizationHeader() = "${Authentication.BEARER} ${getTokens().accessToken}"
-
- internal open class BearerTokensInfo(val bearerTokens: BearerTokens, expiresIn: Int) {
- private val expiryDate: LocalDateTime
-
- init {
- this.expiryDate = calculateExpiryDate(expiresIn)
- }
-
- private fun calculateExpiryDate(expiresIn: Int): LocalDateTime = LocalDateTime.now().plusSeconds(expiresIn.toLong())
-
- open fun isAboutToExpire(): Boolean = LocalDateTime.now().isAfter(expiryDate.minusSeconds(Authentication.BEARER_EXPIRY_DATE_MARGIN))
-
- companion object {
- internal val emptyBearerTokenInfo =
- object : BearerTokensInfo(BearerTokens(Constant.EMPTY_STRING, Constant.EMPTY_STRING), -1) {
- override fun isAboutToExpire() = true
- }
- }
- }
-
- internal data class TokenResponse
- @JsonCreator
- constructor(
- @JsonProperty("access_token") val accessToken: String,
- @JsonProperty("expires_in") val expiresIn: Int,
- @JsonProperty("token_type") val tokenType: String,
- @JsonProperty("scope") val scope: String,
- )
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/RapidAuthenticationStrategy.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/RapidAuthenticationStrategy.kt
deleted file mode 100644
index 0927b756..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/authentication/strategy/RapidAuthenticationStrategy.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.authentication.strategy
-
-import com.expediagroup.sdk.core.constant.Authentication
-import com.expediagroup.sdk.core.constant.Constant
-import com.expediagroup.sdk.core.constant.SignatureValues
-import com.expediagroup.sdk.core.plugin.authentication.AuthenticationConfiguration
-import io.ktor.client.request.HttpRequestBuilder
-import java.math.BigInteger
-import java.nio.charset.StandardCharsets
-import java.security.MessageDigest
-import java.security.spec.MGF1ParameterSpec
-import java.time.Instant
-
-internal class RapidAuthenticationStrategy(private val configs: AuthenticationConfiguration) : AuthenticationStrategy {
- private var signature: String = Constant.EMPTY_STRING
-
- override fun isTokenAboutToExpire(): Boolean = true
-
- override fun renewToken() {
- val credentials = configs.credentials
- signature = calculateSignature(credentials.key, credentials.secret, Instant.now().epochSecond)
- }
-
- override fun isIdentityRequest(request: HttpRequestBuilder) = false
-
- override fun getAuthorizationHeader() = createAuthorizationHeader(signature)
-
- private fun createAuthorizationHeader(signature: String?): String = "${Authentication.EAN} $signature"
-
- private fun calculateSignature(
- apiKey: String,
- secret: String,
- timestamp: Long
- ): String {
- val toBeHashed = apiKey + secret + timestamp
- val messageDigest = MessageDigest.getInstance(MGF1ParameterSpec.SHA512.digestAlgorithm)
- val bytes = messageDigest.digest(toBeHashed.toByteArray(StandardCharsets.UTF_8))
- val signature =
- buildString {
- bytes.forEach {
- append(((it.toInt() and SignatureValues.ONE_BYTE_MASK) + SignatureValues.INCREMENT).toString(SignatureValues.RADIX).substring(BigInteger.ONE.toInt()))
- }
- }
- return "${SignatureValues.API_KEY}=$apiKey,${SignatureValues.SIGNATURE}=$signature,${SignatureValues.TIMESTAMP}=$timestamp"
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/encoding/EncodingConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/encoding/EncodingConfiguration.kt
deleted file mode 100644
index c789bc69..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/encoding/EncodingConfiguration.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.encoding
-
-import com.expediagroup.sdk.core.plugin.KtorPluginConfiguration
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-
-internal data class EncodingConfiguration(
- override val httpClientConfiguration: HttpClientConfig
-) : KtorPluginConfiguration(httpClientConfiguration) {
- companion object {
- fun from(httpClientConfig: HttpClientConfig) = EncodingConfiguration(httpClientConfig)
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/encoding/EncodingPlugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/encoding/EncodingPlugin.kt
deleted file mode 100644
index 658dd636..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/encoding/EncodingPlugin.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.encoding
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.plugin.Plugin
-import io.ktor.client.plugins.compression.ContentEncoding
-
-internal object EncodingPlugin : Plugin {
- override fun install(
- client: Client,
- configurations: EncodingConfiguration
- ) {
- configurations.httpClientConfiguration.install(ContentEncoding) {
- gzip()
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/exception/ExceptionHandlingConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/exception/ExceptionHandlingConfiguration.kt
deleted file mode 100644
index 89cdd14c..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/exception/ExceptionHandlingConfiguration.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.exception
-
-import com.expediagroup.sdk.core.plugin.KtorPluginConfiguration
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-
-internal data class ExceptionHandlingConfiguration(
- override val httpClientConfiguration: HttpClientConfig
-) : KtorPluginConfiguration(httpClientConfiguration) {
- companion object {
- fun from(httpClientConfig: HttpClientConfig) = ExceptionHandlingConfiguration(httpClientConfig)
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/exception/ExceptionHandlingPlugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/exception/ExceptionHandlingPlugin.kt
deleted file mode 100644
index 15d58a13..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/exception/ExceptionHandlingPlugin.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.exception
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.model.exception.handleWith
-import com.expediagroup.sdk.core.model.getTransactionId
-import com.expediagroup.sdk.core.plugin.Plugin
-import io.ktor.client.plugins.HttpResponseValidator
-
-internal object ExceptionHandlingPlugin : Plugin {
- override fun install(
- client: Client,
- configurations: ExceptionHandlingConfiguration
- ) {
- with(configurations.httpClientConfiguration) {
- HttpResponseValidator {
- handleResponseExceptionWithRequest { exception, request ->
- exception.handleWith(request.headers.getTransactionId())
- }
- }
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/httptimeout/HttpTimeoutConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/httptimeout/HttpTimeoutConfiguration.kt
deleted file mode 100644
index a0d28f3c..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/httptimeout/HttpTimeoutConfiguration.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.httptimeout
-
-import com.expediagroup.sdk.core.plugin.KtorPluginConfiguration
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-
-internal data class HttpTimeoutConfiguration(
- override val httpClientConfiguration: HttpClientConfig,
- val requestTimeout: Long,
- val connectionTimeout: Long,
- val socketTimeout: Long
-) : KtorPluginConfiguration(httpClientConfiguration) {
- companion object {
- fun from(
- httpClientConfig: HttpClientConfig,
- requestTimeout: Long,
- connectionTimeout: Long,
- socketTimeout: Long
- ) = HttpTimeoutConfiguration(httpClientConfig, requestTimeout, connectionTimeout, socketTimeout)
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/httptimeout/HttpTimeoutPlugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/httptimeout/HttpTimeoutPlugin.kt
deleted file mode 100644
index 0590d884..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/httptimeout/HttpTimeoutPlugin.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.httptimeout
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.plugin.Plugin
-import io.ktor.client.plugins.HttpTimeout
-
-internal object HttpTimeoutPlugin : Plugin {
- override fun install(
- client: Client,
- configurations: HttpTimeoutConfiguration
- ) {
- configurations.httpClientConfiguration.install(HttpTimeout) {
- requestTimeoutMillis = configurations.requestTimeout
- connectTimeoutMillis = configurations.connectionTimeout
- socketTimeoutMillis = configurations.socketTimeout
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ExpediaGroupLogger.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ExpediaGroupLogger.kt
deleted file mode 100644
index 78c55920..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ExpediaGroupLogger.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.constant.LogMaskingFields
-import com.expediagroup.sdk.core.constant.LoggingMessage.LOGGING_PREFIX
-import org.slf4j.Logger
-
-internal class ExpediaGroupLogger(private val logger: Logger, private val client: Client? = null) : Logger by logger {
- override fun info(msg: String) = logger.info(decorate(msg))
-
- override fun warn(msg: String) = logger.warn(decorate(msg))
-
- override fun debug(msg: String) = logger.debug(decorate(msg))
-
- private fun decorate(msg: String): String = "$LOGGING_PREFIX ${mask(msg, getMaskedBodyFields())}"
-
- private fun getMaskedBodyFields(): Set = client?.getLoggingMaskedFieldsProvider()?.getMaskedBodyFields() ?: LogMaskingFields.DEFAULT_MASKED_BODY_FIELDS
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ExpediaGroupLoggerFactory.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ExpediaGroupLoggerFactory.kt
deleted file mode 100644
index e2d2326e..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ExpediaGroupLoggerFactory.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.client.Client
-import org.slf4j.LoggerFactory
-
-internal object ExpediaGroupLoggerFactory {
- fun getLogger(clazz: Class<*>) = ExpediaGroupLogger(LoggerFactory.getLogger(clazz))
-
- fun getLogger(
- clazz: Class<*>,
- client: Client
- ) = ExpediaGroupLogger(LoggerFactory.getLogger(clazz), client)
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LogMasker.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LogMasker.kt
deleted file mode 100644
index b22a6d64..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LogMasker.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.constant.LogMaskingRegex.NUMBER_FIELD_REGEX
-import com.expediagroup.sdk.core.constant.LoggingMessage.OMITTED
-import com.expediagroup.sdk.core.constant.provider.LogMaskingRegexProvider.getMaskedFieldsRegex
-
-internal fun mask(
- message: String,
- maskedBodyFields: Set
-): String = masks.fold(message) { acc, mask -> mask.mask(acc, maskedBodyFields) }
-
-internal fun interface Mask {
- fun getRegex(maskedBodyFields: Set): Regex
-
- fun mask(
- string: String,
- maskedBodyFields: Set
- ): String = string.replace(this.getRegex(maskedBodyFields)) { maskSubstring(it.value) }
-
- fun maskSubstring(string: String) = OMITTED
-}
-
-internal val masks: List =
- listOf(
- Mask(::getMaskedFieldsRegex),
- Mask { NUMBER_FIELD_REGEX }
- )
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingConfiguration.kt
deleted file mode 100644
index 70787421..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingConfiguration.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.plugin.KtorPluginConfiguration
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-import io.ktor.client.plugins.logging.LogLevel
-import io.ktor.client.plugins.logging.Logger
-
-internal data class LoggingConfiguration(
- override val httpClientConfiguration: HttpClientConfig,
- val maskedLoggingHeaders: Set,
- val maskedLoggingBodyFields: Set,
- val level: LogLevel = LogLevel.ALL,
- val getLogger: (client: Client) -> Logger = createCustomLogger
-) : KtorPluginConfiguration(httpClientConfiguration) {
- companion object {
- fun from(
- httpClientConfig: HttpClientConfig,
- maskedLoggingHeaders: Set,
- maskedLoggingBodyFields: Set
- ) = LoggingConfiguration(httpClientConfig, maskedLoggingHeaders, maskedLoggingBodyFields)
- }
-}
-
-private val createCustomLogger: (client: Client) -> Logger
- get() = {
- object : Logger {
- val delegate = ExpediaGroupLoggerFactory.getLogger(Client::class.java, it)
-
- override fun log(message: String) = delegate.info(message)
- }
- }
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingMaskedFieldsProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingMaskedFieldsProvider.kt
deleted file mode 100644
index 033ff1f6..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingMaskedFieldsProvider.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.constant.LogMaskingFields.DEFAULT_MASKED_BODY_FIELDS
-import com.expediagroup.sdk.core.constant.LogMaskingFields.DEFAULT_MASKED_HEADER_FIELDS
-import com.expediagroup.sdk.core.constant.LogMaskingRegex.FIELD_REGEX
-import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupInvalidFieldNameException
-
-class LoggingMaskedFieldsProvider(maskedLoggingHeaders: Set, maskedLoggingBodyFields: Set) {
- private val maskedHeaderFields: Set
- private val maskedBodyFields: Set
-
- init {
- maskedLoggingHeaders.filter(::isInvalid).takeIf { it.isNotEmpty() }?.let { throw ExpediaGroupInvalidFieldNameException(it) }
- maskedLoggingBodyFields.filter(::isInvalid).takeIf { it.isNotEmpty() }?.let { throw ExpediaGroupInvalidFieldNameException(it) }
- maskedHeaderFields = DEFAULT_MASKED_HEADER_FIELDS.union(maskedLoggingHeaders)
- maskedBodyFields = DEFAULT_MASKED_BODY_FIELDS.union(maskedLoggingBodyFields)
- }
-
- /**
- * @return a copy of the list of headers to be masked
- */
- fun getMaskedHeaderFields(): Set = maskedHeaderFields.toSet()
-
- /**
- * @return a copy of the list of body fields to be masked
- */
- fun getMaskedBodyFields(): Set = maskedBodyFields.toSet()
-
- private fun isInvalid(fieldName: String): Boolean = !fieldName.matches(FIELD_REGEX)
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingPlugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingPlugin.kt
deleted file mode 100644
index 758a5df7..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/LoggingPlugin.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.constant.ExceptionMessage
-import com.expediagroup.sdk.core.constant.LoggingMessage
-import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupClientException
-import com.expediagroup.sdk.core.plugin.Plugin
-import io.ktor.client.plugins.logging.Logging
-
-internal object LoggingPlugin : Plugin {
- val clientLoggingMaskedFieldsProviders = mutableMapOf()
-
- override fun install(
- client: Client,
- configurations: LoggingConfiguration
- ) {
- clientLoggingMaskedFieldsProviders[client] =
- LoggingMaskedFieldsProvider(
- configurations.maskedLoggingHeaders,
- configurations.maskedLoggingBodyFields
- )
- configurations.httpClientConfiguration.install(Logging) {
- logger = configurations.getLogger(client)
- level = configurations.level
- sanitizeHeader(LoggingMessage.OMITTED) { header ->
- client.getLoggingMaskedFieldsProvider().getMaskedHeaderFields().contains(header)
- }
- }
- configurations.httpClientConfiguration.install(RequestBodyLogger)
- configurations.httpClientConfiguration.install(ResponseBodyLogger)
- }
-}
-
-internal fun Client.getLoggingMaskedFieldsProvider(): LoggingMaskedFieldsProvider =
- LoggingPlugin.clientLoggingMaskedFieldsProviders[this] ?: throw ExpediaGroupClientException(ExceptionMessage.LOGGING_MASKED_FIELDS_NOT_CONFIGURED_FOR_CLIENT)
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/RequestBodyLogger.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/RequestBodyLogger.kt
deleted file mode 100644
index 6dcc504e..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/RequestBodyLogger.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.constant.LoggerName
-import com.expediagroup.sdk.core.constant.provider.LoggingMessageProvider
-import com.expediagroup.sdk.core.model.getTransactionId
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.HttpClientPlugin
-import io.ktor.client.request.HttpRequestBuilder
-import io.ktor.client.request.HttpSendPipeline
-import io.ktor.http.content.OutputStreamContent
-import io.ktor.util.AttributeKey
-import io.ktor.util.pipeline.PipelineContext
-import io.ktor.utils.io.ByteChannel
-
-internal class RequestBodyLogger {
- private val log = ExpediaGroupLoggerFactory.getLogger(javaClass)
-
- companion object Plugin : HttpClientPlugin {
- override val key: AttributeKey = AttributeKey(LoggerName.REQUEST_BODY_LOGGER)
-
- override fun install(
- plugin: RequestBodyLogger,
- scope: HttpClient
- ) {
- scope.sendPipeline.intercept(HttpSendPipeline.Monitoring) {
- val body: String = getBody()
- plugin.log.debug(LoggingMessageProvider.getRequestBodyMessage(body, context.headers.getTransactionId()))
- proceed()
- }
- }
-
- private suspend fun PipelineContext.getBody(): String {
- if (context.headers.get("Content-Type")?.contains("multipart") == true) {
- return "<-- Multipart Request -->"
- }
-
- val body = context.body
- if (body is OutputStreamContent) {
- with(ByteChannel()) {
- body.writeTo(this)
- return readRemaining().readText()
- }
- }
- return body.toString()
- }
-
- override fun prepare(block: RequestBodyLoggerConfig.() -> Unit): RequestBodyLogger {
- return RequestBodyLogger()
- }
- }
-}
-
-internal class RequestBodyLoggerConfig
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ResponseBodyLogger.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ResponseBodyLogger.kt
deleted file mode 100644
index a78021f4..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/logging/ResponseBodyLogger.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.logging
-
-import com.expediagroup.sdk.core.constant.HeaderValue
-import com.expediagroup.sdk.core.constant.LoggerName
-import com.expediagroup.sdk.core.constant.provider.LoggingMessageProvider
-import com.expediagroup.sdk.core.model.getTransactionId
-import com.expediagroup.sdk.core.plugin.logging.GZipEncoder.decode
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.HttpClientPlugin
-import io.ktor.client.plugins.compression.ContentEncoder
-import io.ktor.client.statement.HttpResponse
-import io.ktor.client.statement.HttpResponsePipeline
-import io.ktor.client.statement.request
-import io.ktor.http.HttpHeaders
-import io.ktor.util.AttributeKey
-import io.ktor.util.Encoder
-import io.ktor.util.GZip
-import io.ktor.util.InternalAPI
-import io.ktor.utils.io.ByteReadChannel
-
-class ResponseBodyLogger {
- private val log = ExpediaGroupLoggerFactory.getLogger(javaClass)
-
- companion object Plugin : HttpClientPlugin {
- override val key: AttributeKey = AttributeKey(LoggerName.RESPONSE_BODY_LOGGER)
-
- @OptIn(InternalAPI::class)
- override fun install(
- plugin: ResponseBodyLogger,
- scope: HttpClient
- ) {
- scope.responsePipeline.intercept(HttpResponsePipeline.Receive) {
- val response: HttpResponse = context.response
-
- if (response.headers.get(HttpHeaders.ContentType)?.contains("application/json") == false) {
- plugin.log.debug(LoggingMessageProvider.getResponseBodyMessage("<-- Multipart Response -->", response.request.headers.getTransactionId()))
- proceed()
- return@intercept
- }
-
- val byteReadChannel: ByteReadChannel = if (response.contentEncoding().equals(HeaderValue.GZIP)) scope.decode(response.content) else response.content
- val body: String = byteReadChannel.readRemaining().readText()
- plugin.log.debug(LoggingMessageProvider.getResponseBodyMessage(body, response.request.headers.getTransactionId()))
- proceed()
- }
- }
-
- override fun prepare(block: ResponseBodyLoggerConfig.() -> Unit): ResponseBodyLogger {
- return ResponseBodyLogger()
- }
- }
-}
-
-fun HttpResponse.contentEncoding(): String? = headers[HttpHeaders.ContentEncoding]
-
-internal object GZipEncoder : ContentEncoder, Encoder by GZip {
- override val name: String = HeaderValue.GZIP
-}
-
-class ResponseBodyLoggerConfig
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/request/DefaultRequestConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/request/DefaultRequestConfiguration.kt
deleted file mode 100644
index c1114a22..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/request/DefaultRequestConfiguration.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.request
-
-import com.expediagroup.sdk.core.plugin.KtorPluginConfiguration
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-
-internal data class DefaultRequestConfiguration(
- override val httpClientConfiguration: HttpClientConfig,
- val endpoint: String
-) : KtorPluginConfiguration(httpClientConfiguration) {
- companion object {
- fun from(
- httpClientConfig: HttpClientConfig,
- endpoint: String
- ) = DefaultRequestConfiguration(httpClientConfig, endpoint)
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/request/DefaultRequestPlugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/request/DefaultRequestPlugin.kt
deleted file mode 100644
index 745c6771..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/request/DefaultRequestPlugin.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.request
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.plugin.Plugin
-import io.ktor.client.plugins.DefaultRequest
-
-internal object DefaultRequestPlugin : Plugin {
- override fun install(
- client: Client,
- configurations: DefaultRequestConfiguration
- ) {
- configurations.httpClientConfiguration.install(DefaultRequest) {
- url(configurations.endpoint)
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/serialization/SerializationConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/serialization/SerializationConfiguration.kt
deleted file mode 100644
index dc1546df..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/serialization/SerializationConfiguration.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.serialization
-
-import com.expediagroup.sdk.core.plugin.KtorPluginConfiguration
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngineConfig
-import io.ktor.http.ContentType
-
-internal data class SerializationConfiguration(
- override val httpClientConfiguration: HttpClientConfig,
- val contentType: ContentType = ContentType.Application.Json
-) : KtorPluginConfiguration(httpClientConfiguration) {
- companion object {
- fun from(httpClientConfig: HttpClientConfig) = SerializationConfiguration(httpClientConfig)
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/serialization/SerializationPlugin.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/serialization/SerializationPlugin.kt
deleted file mode 100644
index 08153243..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/core/plugin/serialization/SerializationPlugin.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2022 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.plugin.serialization
-
-import com.expediagroup.sdk.core.client.Client
-import com.expediagroup.sdk.core.plugin.Plugin
-import com.fasterxml.jackson.databind.DeserializationFeature
-import com.fasterxml.jackson.databind.PropertyNamingStrategies
-import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
-import io.ktor.serialization.jackson.jackson
-import java.text.SimpleDateFormat
-
-internal object SerializationPlugin : Plugin {
- override fun install(
- client: Client,
- configurations: SerializationConfiguration,
- ) {
- configurations.httpClientConfiguration.install(ContentNegotiation) {
- jackson {
- enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
- disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
- setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
- setDateFormat(SimpleDateFormat())
- findAndRegisterModules()
- }
- }
- }
-}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/ApolloAliases.kt b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/ApolloAliases.kt
new file mode 100644
index 00000000..19537829
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/ApolloAliases.kt
@@ -0,0 +1,15 @@
+package com.expediagroup.sdk.graphql.common
+
+import com.apollographql.apollo.api.http.HttpRequest
+import com.apollographql.apollo.api.http.HttpResponse
+import com.apollographql.apollo.api.Error
+
+typealias ApolloHttpRequest = HttpRequest
+
+typealias ApolloHttpRequestBuilder = HttpRequest.Builder
+
+typealias ApolloHttpResponse = HttpResponse
+
+typealias ApolloHttpResponseBuilder = HttpResponse.Builder
+
+typealias ApolloError = Error
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/ApolloHttpEngine.kt b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/ApolloHttpEngine.kt
new file mode 100644
index 00000000..22345ecd
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/ApolloHttpEngine.kt
@@ -0,0 +1,125 @@
+package com.expediagroup.sdk.graphql.common
+
+import com.apollographql.apollo.api.http.HttpHeader
+import com.apollographql.apollo.api.http.HttpMethod
+import com.apollographql.apollo.exception.ApolloNetworkException
+import com.apollographql.java.client.ApolloDisposable
+import com.apollographql.java.client.network.http.HttpCallback
+import com.apollographql.java.client.network.http.HttpEngine
+import com.expediagroup.sdk.core.client.RequestExecutor
+import com.expediagroup.sdk.core.http.MediaType
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.RequestBody
+import com.expediagroup.sdk.core.http.Response
+import java.io.IOException
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicBoolean
+import okio.BufferedSink
+
+class ApolloHttpEngine(
+ private val requestExecutor: RequestExecutor
+) : HttpEngine {
+ private val activeRequests = ConcurrentHashMap()
+ private val isDisposed = AtomicBoolean(false)
+
+ override fun execute(request: ApolloHttpRequest, callback: HttpCallback, disposable: ApolloDisposable) {
+ if (isDisposed.get()) {
+ callback.onFailure(ApolloNetworkException("HTTP engine has been disposed"))
+ return
+ }
+
+ val requestId = UUID.randomUUID().toString()
+ activeRequests[requestId] = disposable
+
+ try {
+ validateRequest(request)
+ val sdkResponse = requestExecutor.execute(buildSdkRequestFromApolloRequest(request))
+
+ if (!isDisposed.get()) {
+ callback.onResponse(buildApolloResponseFromSdkResponse(sdkResponse))
+ }
+ } catch (e: Exception) {
+ callback.onFailure(ApolloNetworkException("Unexpected error occurred", e))
+ } finally {
+ activeRequests.remove(requestId)
+ }
+ }
+
+ override fun dispose() {
+ if (isDisposed.compareAndSet(false, true)) {
+ activeRequests.values.forEach { it.dispose() }
+ activeRequests.clear()
+ }
+ }
+
+ private fun buildSdkRequestFromApolloRequest(apolloRequest: ApolloHttpRequest): Request {
+ return Request.Builder()
+ .url(apolloRequest.url)
+ .apply {
+ addSdkRequestHeadersFromApolloRequest(apolloRequest, this)
+ addSdkRequestBodyFromApolloRequest(apolloRequest, this)
+ }
+ .build()
+ }
+
+ private fun buildApolloResponseFromSdkResponse(sdkHttpResponse: Response): ApolloHttpResponse {
+ return ApolloHttpResponseBuilder(sdkHttpResponse.code).apply {
+ addApolloResponseHeadersFromSdkResponse(sdkHttpResponse, this)
+ addApolloResponseBodyFromSdkResponse(sdkHttpResponse, this)
+ }.build()
+ }
+
+ private fun addSdkRequestHeadersFromApolloRequest(
+ request: ApolloHttpRequest,
+ sdkRequestBuilder: Request.Builder
+ ) {
+ request.headers.forEach { apolloHeader ->
+ sdkRequestBuilder.addHeader(apolloHeader.name, apolloHeader.value)
+ }
+ sdkRequestBuilder.addHeader("Content-Type", "application/json")
+ }
+
+ private fun addSdkRequestBodyFromApolloRequest(request: ApolloHttpRequest, sdkRequestBuilder: Request.Builder) {
+ if (request.method != HttpMethod.Post) {
+ throw UnsupportedOperationException("Only POST requests are supported for GraphQL")
+ }
+
+ request.body?.let { requestBody ->
+ sdkRequestBuilder.method("POST", object : RequestBody() {
+ override fun mediaType(): MediaType? = MediaType.parse(requestBody.contentType)
+ override fun contentLength(): Long = requestBody.contentLength
+
+ @Throws(IOException::class)
+ override fun writeTo(sink: BufferedSink) = requestBody.writeTo(sink)
+ })
+ }
+ }
+
+ private fun addApolloResponseHeadersFromSdkResponse(
+ sdkHttpResponse: Response,
+ apolloHttpResponseBuilder: ApolloHttpResponseBuilder
+ ) {
+ sdkHttpResponse.headers.names().mapNotNull { headerName ->
+ sdkHttpResponse.headers.get(headerName)?.let { headerValue ->
+ HttpHeader(name = headerName, value = headerValue)
+ }
+ }.let {
+ apolloHttpResponseBuilder.headers(it)
+ }
+ }
+
+ private fun addApolloResponseBodyFromSdkResponse(
+ sdkHttpResponse: Response,
+ apolloHttpResponseBuilder: ApolloHttpResponseBuilder
+ ) {
+ sdkHttpResponse.body?.let {
+ apolloHttpResponseBuilder.body(it.source())
+ }
+ }
+
+ private fun validateRequest(request: ApolloHttpRequest) {
+ require(request.url.isNotBlank()) { "Request URL cannot be blank" }
+ require(request.method == HttpMethod.Post) { "Only POST requests are supported for GraphQL" }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/DefaultGraphQLExecutor.kt b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/DefaultGraphQLExecutor.kt
index 59a359d5..23cafb11 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/DefaultGraphQLExecutor.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/DefaultGraphQLExecutor.kt
@@ -16,22 +16,23 @@
package com.expediagroup.sdk.graphql.common
-import com.apollographql.apollo.ApolloClient
+import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.Mutation
+import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.Query
-import com.apollographql.ktor.ktorClient
-import com.expediagroup.sdk.core.client.ExpediaGroupClient
-import com.expediagroup.sdk.core.configuration.ExpediaGroupClientConfiguration
+import com.apollographql.java.client.ApolloClient
+import com.expediagroup.sdk.core.client.RequestExecutor
import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupServiceException
-import com.expediagroup.sdk.graphql.extension.toSDKError
import com.expediagroup.sdk.graphql.model.exception.NoDataException
+import com.expediagroup.sdk.graphql.model.response.Error
import com.expediagroup.sdk.graphql.model.response.RawResponse
-import io.ktor.client.statement.HttpResponse
-import kotlinx.coroutines.runBlocking
+import java.util.concurrent.CompletableFuture
+
/**
- * Default implementation of [GraphQLExecutor], responsible for executing GraphQL queries and mutations
- * using Apollo Kotlin with a custom HTTP client.
+ * A streamlined implementation of [GraphQLExecutor] that handles GraphQL operations with robust
+ * error handling and clean response processing. This executor processes both queries and mutations
+ * while providing detailed error information when operations fail.
*
* This executor leverages the Apollo Client to perform requests and processes responses by capturing
* the entire data structure and any errors in a [RawResponse], which can then be further processed or
@@ -39,52 +40,60 @@ import kotlinx.coroutines.runBlocking
*
* By default - this implementation is used internally in all higher-level clients that extend [GraphQLClient] abstract class
*
- * @param config Configuration details required to set up the custom client and Apollo Client.
+ * @param requestExecutor used for HTTP request execution within the SDK
+ * @param serverUrl GraphQL server URL
*/
-internal class DefaultGraphQLExecutor(config: ExpediaGroupClientConfiguration) : GraphQLExecutor() {
-
- // Custom client for handling HTTP requests and responses.
- private val expediaGroupClient =
- object : ExpediaGroupClient(clientConfiguration = config, namespace = "lodging-connectivity-sdk") {
- override suspend fun throwServiceException(response: HttpResponse, operationId: String) {
- throw ExpediaGroupServiceException("Service error occurred for operation $operationId.\nResponse: $response")
- }
- }
+internal class DefaultGraphQLExecutor(requestExecutor: RequestExecutor, serverUrl: String) : GraphQLExecutor() {
/**
* The Apollo Client used to execute GraphQL requests, configured with a custom HTTP client.
*/
- override val apolloClient: ApolloClient = ApolloClient
- .Builder()
- .serverUrl(config.endpoint!!)
- .ktorClient(expediaGroupClient.httpClient)
+ override val apolloClient: ApolloClient = ApolloClient.Builder()
+ .serverUrl(serverUrl)
+ .httpEngine(ApolloHttpEngine(requestExecutor))
.build()
+
+ /**
+ * Asynchronously executes a GraphQL query and returns a [CompletableFuture] containing the complete
+ * data and any errors wrapped in [RawResponse].
+ *
+ * @param query The GraphQL query to be executed.
+ * @return A [CompletableFuture] with the full data structure and any errors from the server.
+ * @throws [ExpediaGroupServiceException] If an exception occurs during query execution.
+ * @throws [NoDataException] If the query completes without data but includes errors.
+ */
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
+ override fun executeAsync(query: Query): CompletableFuture> {
+ return CompletableFuture>().also {
+ apolloClient.query(query).enqueue { response -> processOperationResponse(response, it) }
+ }
+ }
+
/**
* Executes a GraphQL query and returns a [RawResponse] containing the complete data and any errors.
*
* @param query The GraphQL query to be executed.
* @return A [RawResponse] with the full data structure and any errors from the server.
- * @throws ExpediaGroupServiceException If an exception occurs during query execution.
- * @throws NoDataException If the query completes without data but includes errors.
+ * @throws [ExpediaGroupServiceException] If an exception occurs during query execution.
+ * @throws [NoDataException] If the query completes without data but includes errors.
*/
- override fun execute(query: Query): RawResponse {
- return runBlocking {
- apolloClient.query(query).execute().apply {
- if (exception != null) {
- throw ExpediaGroupServiceException(message = exception?.message, cause = exception)
- }
- if (data == null && hasErrors()) {
- throw NoDataException(
- message = "No data received from the server",
- errors = errors!!.map { it.toSDKError() })
- }
- }.let {
- RawResponse(
- data = it.data!!,
- errors = it.errors?.map { apolloError -> apolloError.toSDKError() }
- )
- }
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
+ override fun execute(query: Query): RawResponse = executeAsync(query).get()
+
+ /**
+ * Asynchronously executes a GraphQL mutation and returns a [CompletableFuture] containing the complete
+ * data and any errors wrapped in [RawResponse].
+ *
+ * @param mutation The GraphQL mutation to be executed.
+ * @return A [CompletableFuture] with the full data structure and any errors from the server.
+ * @throws [ExpediaGroupServiceException] If an exception occurs during mutation execution.
+ * @throws [NoDataException] If the mutation completes without data but includes errors.
+ */
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
+ override fun executeAsync(mutation: Mutation): CompletableFuture> {
+ return CompletableFuture>().also {
+ apolloClient.mutation(mutation).enqueue { response -> processOperationResponse(response, it) }
}
}
@@ -93,26 +102,53 @@ internal class DefaultGraphQLExecutor(config: ExpediaGroupClientConfiguration) :
*
* @param mutation The GraphQL mutation to be executed.
* @return A [RawResponse] with the full data structure and any errors from the server.
- * @throws ExpediaGroupServiceException If an exception occurs during mutation execution.
- * @throws NoDataException If the mutation completes without data but includes errors.
+ * @throws [ExpediaGroupServiceException] If an exception occurs during mutation execution.
+ * @throws [NoDataException] If the mutation completes without data but includes errors.
*/
- override fun execute(mutation: Mutation): RawResponse {
- return runBlocking {
- apolloClient.mutation(mutation).execute().apply {
- if (exception != null) {
- throw ExpediaGroupServiceException(message = exception?.message, cause = exception)
- }
- if (data == null && hasErrors()) {
- throw NoDataException(
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
+ override fun execute(mutation: Mutation): RawResponse = executeAsync(mutation).get()
+
+
+ /**
+ * Handles the response from a GraphQL operation, determining whether to complete the provided CompletableFuture
+ * with either success or an exception based on the response data and errors.
+ *
+ * @param response The ApolloResponse containing the data and errors from the GraphQL operation.
+ * @param future A CompletableFuture that will be completed based on the response handling logic.
+ */
+ private fun processOperationResponse(
+ response: ApolloResponse,
+ future: CompletableFuture>
+ ) {
+ try {
+ when {
+ response.exception != null -> future.completeExceptionally(
+ ExpediaGroupServiceException(
+ message = response.exception?.message,
+ cause = response.exception
+ )
+ )
+
+ response.data != null && response.hasErrors() -> future.completeExceptionally(
+ NoDataException(
message = "No data received from the server",
- errors = errors!!.map { it.toSDKError() })
- }
- }.let {
- RawResponse(
- data = it.data!!,
- errors = it.errors?.map { apolloError -> apolloError.toSDKError() }
+ errors = response.errors!!.map { Error.fromApolloError(it) })
+ )
+
+ else -> future.complete(
+ RawResponse(
+ data = response.data!!,
+ errors = response.errors?.map { Error.fromApolloError(it) }
+ )
)
}
+ } catch (e: Exception) {
+ future.completeExceptionally(
+ ExpediaGroupServiceException(
+ message = e.message,
+ cause = e
+ )
+ )
}
}
}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLClient.kt
index f44c39f9..483c2c83 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLClient.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLClient.kt
@@ -16,6 +16,8 @@
package com.expediagroup.sdk.graphql.common
+import com.expediagroup.sdk.lodgingconnectivity.configuration.ApiEndpoint
+
/**
* Abstract base class for GraphQL clients that defines the core structure for executing GraphQL operations.
* Classes extending `GraphQLClient` are expected to provide an implementation of the [GraphQLExecutor].
@@ -24,6 +26,13 @@ package com.expediagroup.sdk.graphql.common
* while relying on the `graphQLExecutor` to perform the actual request handling.
*/
abstract class GraphQLClient {
+
+ /**
+ * The API endpoint that the client is configured to communicate with. Includes the primary API endpoint
+ * and the authentication endpoint.
+ */
+ protected abstract val apiEndpoint: ApiEndpoint
+
/**
* The executor responsible for handling GraphQL operations.
* Subclasses must provide a concrete implementation of this executor to define
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLExecutor.kt b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLExecutor.kt
index 6b5a8b68..54756e54 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLExecutor.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/graphql/common/GraphQLExecutor.kt
@@ -16,11 +16,13 @@
package com.expediagroup.sdk.graphql.common
-import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Query
+import com.apollographql.java.client.ApolloClient
import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupServiceException
+import com.expediagroup.sdk.graphql.model.exception.NoDataException
import com.expediagroup.sdk.graphql.model.response.RawResponse
+import java.util.concurrent.CompletableFuture
/**
* Abstract base class for executing GraphQL operations, providing a structure for executing queries and mutations
@@ -43,9 +45,10 @@ abstract class GraphQLExecutor {
*
* @param query The GraphQL query to be executed.
* @return A [RawResponse] containing the full data and any errors from the query response.
- * @throws ExpediaGroupServiceException If an exception occurs during the execution of the query.
- * @throws NoDataException If the query completes without data but includes errors.
+ * @throws [ExpediaGroupServiceException] If an exception occurs during the execution of the query.
+ * @throws [NoDataException] If the query completes without data but includes errors.
*/
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
abstract fun execute(query: Query): RawResponse
/**
@@ -53,10 +56,31 @@ abstract class GraphQLExecutor {
*
* @param mutation The GraphQL mutation to be executed.
* @return A [RawResponse] containing the full data and any errors from the mutation response.
- * @throws ExpediaGroupServiceException If an exception occurs during the execution of the mutation.
- * @throws NoDataException If the mutation completes without data but includes errors.
+ * @throws [ExpediaGroupServiceException] If an exception occurs during the execution of the mutation.
+ * @throws [NoDataException] If the mutation completes without data but includes errors.
*/
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
abstract fun execute(mutation: Mutation): RawResponse
-}
+ /**
+ * Asynchronously executes a GraphQL query and returns the complete raw response in a [CompletableFuture].
+ *
+ * @param query The GraphQL query to be executed.
+ * @return A [CompletableFuture] containing the full data and any errors from the query response wrapped in [RawResponse].
+ * @throws [ExpediaGroupServiceException] If an exception occurs during the execution of the query.
+ * @throws [NoDataException] If the query completes without data but includes errors.
+ */
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
+ abstract fun executeAsync(query: Query): CompletableFuture>
+ /**
+ * Asynchronously executes a GraphQL mutation and returns the complete raw response in a [CompletableFuture].
+ *
+ * @param mutation The GraphQL mutation to be executed.
+ * @return A [CompletableFuture] containing the full data and any errors from the mutation response wrapped in [RawResponse].
+ * @throws [ExpediaGroupServiceException] If an exception occurs during the execution of the mutation.
+ * @throws [NoDataException] If the mutation completes without data but includes errors.
+ */
+ @Throws(NoDataException::class, ExpediaGroupServiceException::class)
+ abstract fun executeAsync(mutation: Mutation): CompletableFuture>
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/graphql/extension/ApolloErrorExtension.kt b/code/src/main/kotlin/com/expediagroup/sdk/graphql/extension/ApolloErrorExtension.kt
deleted file mode 100644
index 2963f44d..00000000
--- a/code/src/main/kotlin/com/expediagroup/sdk/graphql/extension/ApolloErrorExtension.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.graphql.extension
-
-import com.apollographql.apollo.api.Error
-
-fun Error.toSDKError() =
- com.expediagroup.sdk.graphql.model.response.Error(
- message = this.message,
- path = this.path?.map { it.toString() }
- )
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/graphql/model/response/Error.kt b/code/src/main/kotlin/com/expediagroup/sdk/graphql/model/response/Error.kt
index 208519c0..ba871d3c 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/graphql/model/response/Error.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/graphql/model/response/Error.kt
@@ -16,6 +16,8 @@
package com.expediagroup.sdk.graphql.model.response
+import com.expediagroup.sdk.graphql.common.ApolloError
+
/**
* Represents an error returned from a GraphQL operation.
*
@@ -25,5 +27,14 @@ package com.expediagroup.sdk.graphql.model.response
*/
data class Error(
val message: String,
- val path: List?,
-)
+ val path: List?
+) {
+ companion object {
+ fun fromApolloError(apolloError: ApolloError): Error {
+ return Error(
+ message = apolloError.message,
+ path = apolloError.path?.map { it.toString() }
+ )
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/common/DefaultRequestExecutor.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/common/DefaultRequestExecutor.kt
new file mode 100644
index 00000000..029845f9
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/common/DefaultRequestExecutor.kt
@@ -0,0 +1,57 @@
+package com.expediagroup.sdk.lodgingconnectivity.common
+
+import com.expediagroup.sdk.core.authentication.bearer.BearerAuthenticationInterceptor
+import com.expediagroup.sdk.core.authentication.bearer.BearerAuthenticationManager
+import com.expediagroup.sdk.core.authentication.common.Credentials
+import com.expediagroup.sdk.core.client.RequestExecutor
+import com.expediagroup.sdk.core.client.Transport
+import com.expediagroup.sdk.core.http.Request
+import com.expediagroup.sdk.core.http.Response
+import com.expediagroup.sdk.core.interceptor.Interceptor
+import com.expediagroup.sdk.core.interceptor.InterceptorsChainExecutor
+import com.expediagroup.sdk.core.logging.LoggingInterceptor
+import com.expediagroup.sdk.core.logging.common.LoggerDecorator
+import com.expediagroup.sdk.core.okhttp.BaseOkHttpClient
+import com.expediagroup.sdk.core.okhttp.OkHttpTransport
+import com.expediagroup.sdk.lodgingconnectivity.configuration.ApiEndpoint
+import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientConfiguration
+import com.expediagroup.sdk.lodgingconnectivity.configuration.CustomClientConfiguration
+import com.expediagroup.sdk.lodgingconnectivity.configuration.DefaultClientConfiguration
+import org.slf4j.LoggerFactory
+
+internal fun getHttpTransport(configuration: ClientConfiguration): Transport = when (configuration) {
+ is CustomClientConfiguration -> configuration.transport
+ is DefaultClientConfiguration -> OkHttpTransport(BaseOkHttpClient.getConfiguredInstance(configuration.buildOkHttpConfiguration()))
+}
+
+class DefaultRequestExecutor(
+ configuration: ClientConfiguration,
+ apiEndpoint: ApiEndpoint
+) : RequestExecutor(getHttpTransport(configuration)) {
+
+ override val interceptors: List = listOf(
+ LoggingInterceptor(logger),
+ BearerAuthenticationInterceptor(
+ BearerAuthenticationManager(
+ transport = this.transport,
+ authUrl = apiEndpoint.authEndpoint,
+ credentials = Credentials(configuration.key, configuration.secret)
+ )
+ )
+ )
+
+ override fun execute(request: Request): Response {
+ val chainExecutor = InterceptorsChainExecutor(
+ interceptors = interceptors,
+ request = request,
+ transport = this.transport
+ )
+
+ return chainExecutor.proceed(request)
+ }
+
+ companion object {
+ private val logger = LoggerDecorator(LoggerFactory.getLogger(this::class.java.enclosingClass))
+ }
+}
+
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/ClientConfiguration.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/ClientConfiguration.kt
index 3bc882ba..02ebfc6d 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/ClientConfiguration.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/ClientConfiguration.kt
@@ -16,147 +16,175 @@
package com.expediagroup.sdk.lodgingconnectivity.configuration
-import com.expediagroup.sdk.core.configuration.ExpediaGroupClientConfiguration
-
-/**
- * A configuration class that holds the necessary credentials and settings for API clients.
- *
- * This class is used to configure SDK clients by providing essential
- * details such as API keys, environment, timeouts, and logging settings.
- *
- * It also provides a fluent `Builder` pattern for easy creation of configuration instances.
- *
- * @property key The API key used for authentication.
- * @property secret The API secret used for authentication.
- * @property environment The environment in which the API client will operate (e.g., production or test).
- * @property requestTimeout The request timeout duration in milliseconds (optional).
- * @property connectionTimeout The connection timeout duration in milliseconds (optional).
- * @property socketTimeout The socket timeout duration in milliseconds (optional).
- * @property maskedLoggingHeaders A set of HTTP headers whose values should be masked in logs (optional).
- * @property maskedLoggingBodyFields A set of fields in the request body whose values should be masked in logs (optional).
- */
-data class ClientConfiguration(
- val key: String?,
- val secret: String?,
- val environment: ClientEnvironment?,
- val requestTimeout: Long? = null,
- val connectionTimeout: Long? = null,
- val socketTimeout: Long? = null,
- val maskedLoggingHeaders: Set? = null,
- val maskedLoggingBodyFields: Set? = null
+import com.expediagroup.sdk.core.client.Transport
+import com.expediagroup.sdk.core.okhttp.OkHttpClientConfiguration
+import okhttp3.ConnectionPool
+import okhttp3.Interceptor
+
+sealed class ClientConfiguration(
+ open val key: String,
+ open val secret: String,
+ open val environment: ClientEnvironment? = null,
) {
- /**
- * A builder for creating `ClientConfiguration` instances.
- */
+ companion object {
+ @JvmStatic
+ fun builder(): DefaultClientConfiguration.Builder = DefaultClientConfiguration.Builder()
+
+ @JvmStatic
+ fun builder(transport: Transport): CustomClientConfiguration.Builder =
+ CustomClientConfiguration.Builder(transport)
+ }
+}
+
+data class DefaultClientConfiguration(
+ override val key: String,
+ override val secret: String,
+ override val environment: ClientEnvironment? = null,
+ val interceptors: List? = null,
+ val networkInterceptors: List? = null,
+ val connectionPool: ConnectionPool? = null,
+ val retryOnConnectionFailure: Boolean? = null,
+ val callTimeout: Int? = null,
+ val connectTimeout: Int? = null,
+ val readTimeout: Int? = null,
+ val writeTimeout: Int? = null
+) : ClientConfiguration(key, secret, environment) {
+
+ fun buildOkHttpConfiguration() = OkHttpClientConfiguration(
+ interceptors = interceptors,
+ networkInterceptors = networkInterceptors,
+ connectionPool = connectionPool,
+ retryOnConnectionFailure = retryOnConnectionFailure,
+ callTimeout = callTimeout,
+ connectTimeout = connectTimeout,
+ readTimeout = readTimeout,
+ writeTimeout = writeTimeout
+ )
+
class Builder {
private var key: String? = null
private var secret: String? = null
private var environment: ClientEnvironment? = null
- private var requestTimeout: Long? = null
- private var connectionTimeout: Long? = null
- private var socketTimeout: Long? = null
- private var maskedLoggingHeaders: Set? = null
- private var maskedLoggingBodyFields: Set? = null
-
- /**
- * Sets the API key.
- * @param key The API key to use.
- */
- fun key(key: String) = apply {
+ private var interceptors: List? = null
+ private var networkInterceptors: List? = null
+ private var connectionPool: ConnectionPool? = null
+ private var retryOnConnectionFailure: Boolean? = null
+ private var callTimeout: Int? = null
+ private var connectTimeout: Int? = null
+ private var readTimeout: Int? = null
+ private var writeTimeout: Int? = null
+
+ fun key(key: String?) = apply {
this.key = key
}
- /**
- * Sets the API secret.
- * @param secret The API secret to use.
- */
- fun secret(secret: String) = apply {
+ fun secret(secret: String?) = apply {
this.secret = secret
}
- /**
- * Sets the environment (e.g., production, test, or sandbox).
- * @param environment The `ClientEnvironment` to use.
- */
fun environment(environment: ClientEnvironment) = apply {
this.environment = environment
}
- /**
- * Sets the request timeout in milliseconds.
- * @param requestTimeout The request timeout duration.
- */
- fun requestTimeout(requestTimeout: Long) = apply {
- this.requestTimeout = requestTimeout
- }
-
- /**
- * Sets the connection timeout in milliseconds.
- * @param connectionTimeout The connection timeout duration.
- */
- fun connectionTimeout(connectionTimeout: Long) = apply {
- this.connectionTimeout = connectionTimeout
- }
-
- /**
- * Sets the socket timeout in milliseconds.
- * @param socketTimeout The socket timeout duration.
- */
- fun socketTimeout(socketTimeout: Long) = apply {
- this.socketTimeout = socketTimeout
- }
-
- /**
- * Sets the headers whose values should be masked in logs.
- * @param maskedLoggingHeaders A set of HTTP headers to mask in logs.
- */
- fun maskedLoggingHeaders(maskedLoggingHeaders: Set) = apply {
- this.maskedLoggingHeaders = maskedLoggingHeaders
- }
-
- /**
- * Sets the body fields whose values should be masked in logs.
- * @param maskedLoggingBodyFields A set of fields in the request body to mask in logs.
- */
- fun maskedLoggingBodyFields(maskedLoggingBodyFields: Set) = apply {
- this.maskedLoggingBodyFields = maskedLoggingBodyFields
- }
-
- /**
- * Builds and returns the `ClientConfiguration` instance.
- * @return The configured `ClientConfiguration`.
- */
- fun build(): ClientConfiguration {
- return ClientConfiguration(
- key,
- secret,
- environment,
- requestTimeout,
- connectionTimeout,
- socketTimeout,
- maskedLoggingHeaders,
- maskedLoggingBodyFields
+ fun interceptors(interceptors: List) = apply {
+ this.interceptors = interceptors
+ }
+
+ fun networkInterceptors(networkInterceptors: List) = apply {
+ this.networkInterceptors = networkInterceptors
+ }
+
+ fun connectionPool(connectionPool: ConnectionPool) = apply {
+ this.connectionPool = connectionPool
+ }
+
+ fun retryOnConnectionFailure(retryOnConnectionFailure: Boolean) = apply {
+ this.retryOnConnectionFailure = retryOnConnectionFailure
+ }
+
+ fun callTimeout(callTimeout: Int) = apply {
+ this.callTimeout = callTimeout
+ }
+
+ fun connectTimeout(connectTimeout: Int) = apply {
+ this.connectTimeout = connectTimeout
+ }
+
+ fun readTimeout(readTimeout: Int) = apply {
+ this.readTimeout = readTimeout
+ }
+
+ fun writeTimeout(writeTimeout: Int) = apply {
+ this.writeTimeout = writeTimeout
+ }
+
+ fun build(): DefaultClientConfiguration {
+ require(key != null) {
+ "key is required"
+ }
+
+ require(secret != null) {
+ "secret is required"
+ }
+
+ return DefaultClientConfiguration(
+ key = key!!,
+ secret = secret!!,
+ environment = environment,
+ interceptors = interceptors,
+ networkInterceptors = networkInterceptors,
+ connectionPool = connectionPool,
+ retryOnConnectionFailure = retryOnConnectionFailure,
+ callTimeout = callTimeout,
+ connectTimeout = connectTimeout,
+ readTimeout = readTimeout,
+ writeTimeout = writeTimeout
)
}
}
+}
- companion object {
- @JvmStatic
- fun builder(): Builder = Builder()
- }
+data class CustomClientConfiguration(
+ override val key: String,
+ override val secret: String,
+ override val environment: ClientEnvironment,
+ val transport: Transport
+) : ClientConfiguration(key, secret, environment) {
- internal fun toExpediaGroupClientConfiguration(apiEndpoint: ApiEndpoint) =
- ExpediaGroupClientConfiguration(
- key = this.key,
- secret = this.secret,
- endpoint = apiEndpoint.endpoint,
- authEndpoint = apiEndpoint.authEndpoint,
- requestTimeout = this.requestTimeout,
- connectionTimeout = this.connectionTimeout,
- socketTimeout = this.socketTimeout,
- maskedLoggingHeaders = this.maskedLoggingHeaders,
- maskedLoggingBodyFields = this.maskedLoggingBodyFields
- )
-}
+ class Builder(private var transport: Transport) {
+ private var key: String? = null
+ private var secret: String? = null
+ private var environment: ClientEnvironment? = null
+ fun key(key: String) = apply {
+ this.key = key
+ }
+
+ fun secret(secret: String) = apply {
+ this.secret = secret
+ }
+
+
+ fun environment(environment: ClientEnvironment) = apply {
+ this.environment = environment
+ }
+
+ fun build(): CustomClientConfiguration {
+ require(key != null) {
+ "key is required"
+ }
+
+ require(secret != null) {
+ "secret is required"
+ }
+
+ return CustomClientConfiguration(
+ key = key!!,
+ secret = secret!!,
+ environment = environment!!,
+ transport = transport
+ )
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/EndpointProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/EndpointProvider.kt
index bc36af1a..0ba4ecfd 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/EndpointProvider.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/EndpointProvider.kt
@@ -37,6 +37,11 @@ enum class SandboxApiEndpoint(val url: String) {
SANDBOX_TEST("https://test-api.sandbox.expediagroup.com/supply/lodging-sandbox/graphql")
}
+enum class FileManagementClientEndpoint(val url: String) {
+ PROD("https://api.expediagroup.com/supply-lodging/v1/files"),
+ TEST("https://test-api.expediagroup.com/supply-lodging/v1/files")
+}
+
enum class AuthEndpoint(val url: String) {
PROD("https://api.expediagroup.com/identity/oauth2/v3/token/"),
TEST("https://test-api.expediagroup.com/identity/oauth2/v3/token/"),
@@ -93,6 +98,14 @@ internal object EndpointProvider {
}
}
+ fun getFileManagementClientEndpoint(environment: ClientEnvironment): String {
+ return try {
+ FileManagementClientEndpoint.valueOf(environment.name).url
+ } catch (e: IllegalArgumentException) {
+ throw IllegalArgumentException("Unsupported environment [$environment] for FileManagementClient")
+ }
+ }
+
fun getAuthEndpoint(environment: ClientEnvironment): String {
return try {
AuthEndpoint.valueOf(environment.name).url
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/FileManagementApiEndpointProvider.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/FileManagementApiEndpointProvider.kt
new file mode 100644
index 00000000..74e82a0b
--- /dev/null
+++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/configuration/FileManagementApiEndpointProvider.kt
@@ -0,0 +1,25 @@
+package com.expediagroup.sdk.lodgingconnectivity.configuration
+
+/**
+ * Provides needed endpoints for EG lodging connectivity File Management API, configured based on the specified client environment.
+ */
+class FileManagementApiEndpointProvider private constructor() {
+ companion object {
+ /**
+ * Returns an [ApiEndpoint] configured for the specified environment.
+ *
+ * This method selects the appropriate API and authentication endpoints based on the given
+ * [ClientEnvironment] to ensure compatibility with different environments (e.g., PROD, TEST).
+ *
+ * @param environment The [ClientEnvironment] specifying the target environment (e.g., PROD, TEST).
+ * @return An [ApiEndpoint] containing the appropriate endpoints for the specified environment.
+ */
+ @JvmStatic
+ fun forEnvironment(environment: ClientEnvironment): ApiEndpoint {
+ return ApiEndpoint(
+ endpoint = EndpointProvider.getFileManagementClientEndpoint(environment),
+ authEndpoint = EndpointProvider.getAuthEndpoint(environment)
+ )
+ }
+ }
+}
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt
index 62409034..df35a674 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt
@@ -19,6 +19,7 @@ package com.expediagroup.sdk.lodgingconnectivity.payment
import com.expediagroup.sdk.graphql.common.DefaultGraphQLExecutor
import com.expediagroup.sdk.graphql.common.GraphQLClient
import com.expediagroup.sdk.graphql.common.GraphQLExecutor
+import com.expediagroup.sdk.lodgingconnectivity.common.DefaultRequestExecutor
import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientConfiguration
import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientEnvironment
import com.expediagroup.sdk.lodgingconnectivity.configuration.PaymentApiEndpointProvider
@@ -35,12 +36,11 @@ import com.expediagroup.sdk.lodgingconnectivity.payment.operation.getPaymentInst
* or timeouts.
*/
class PaymentClient(config: ClientConfiguration) : GraphQLClient() {
+ override val apiEndpoint = PaymentApiEndpointProvider.forEnvironment(config.environment ?: ClientEnvironment.PROD)
+
override val graphQLExecutor: GraphQLExecutor = DefaultGraphQLExecutor(
- config.toExpediaGroupClientConfiguration(
- apiEndpoint = PaymentApiEndpointProvider.forEnvironment(
- environment = config.environment ?: ClientEnvironment.PROD
- ),
- )
+ requestExecutor = DefaultRequestExecutor(config, apiEndpoint),
+ serverUrl = apiEndpoint.endpoint
)
/**
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt
index f3dd088d..b3eb89a2 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt
@@ -19,6 +19,7 @@ package com.expediagroup.sdk.lodgingconnectivity.sandbox
import com.expediagroup.sdk.graphql.common.DefaultGraphQLExecutor
import com.expediagroup.sdk.graphql.common.GraphQLClient
import com.expediagroup.sdk.graphql.common.GraphQLExecutor
+import com.expediagroup.sdk.lodgingconnectivity.common.DefaultRequestExecutor
import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientConfiguration
import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientEnvironment
import com.expediagroup.sdk.lodgingconnectivity.configuration.SandboxApiEndpointProvider
@@ -73,12 +74,11 @@ import com.expediagroup.sdk.lodgingconnectivity.sandbox.reservation.paginator.Sa
* or timeouts.
*/
class SandboxDataManagementClient(config: ClientConfiguration) : GraphQLClient() {
+ override val apiEndpoint = SandboxApiEndpointProvider.forEnvironment(config.environment ?: ClientEnvironment.SANDBOX_PROD)
+
override val graphQLExecutor: GraphQLExecutor = DefaultGraphQLExecutor(
- config.toExpediaGroupClientConfiguration(
- apiEndpoint = SandboxApiEndpointProvider.forEnvironment(
- environment = config.environment ?: ClientEnvironment.SANDBOX_PROD
- ),
- )
+ requestExecutor = DefaultRequestExecutor(config, apiEndpoint),
+ serverUrl = apiEndpoint.endpoint
)
/**
diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt
index e9e22972..7ce34c30 100644
--- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt
+++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt
@@ -19,10 +19,10 @@ package com.expediagroup.sdk.lodgingconnectivity.supply.reservation
import com.expediagroup.sdk.graphql.common.DefaultGraphQLExecutor
import com.expediagroup.sdk.graphql.common.GraphQLClient
import com.expediagroup.sdk.graphql.common.GraphQLExecutor
+import com.expediagroup.sdk.lodgingconnectivity.common.DefaultRequestExecutor
import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientConfiguration
import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientEnvironment
import com.expediagroup.sdk.lodgingconnectivity.configuration.SupplyApiEndpointProvider
-import com.expediagroup.sdk.lodgingconnectivity.sandbox.operation.type.CreatePropertyInput
import com.expediagroup.sdk.lodgingconnectivity.supply.operation.type.CancelReservationInput
import com.expediagroup.sdk.lodgingconnectivity.supply.operation.type.CancelReservationReconciliationInput
import com.expediagroup.sdk.lodgingconnectivity.supply.operation.type.CancelVrboReservationInput
@@ -59,12 +59,12 @@ import com.expediagroup.sdk.lodgingconnectivity.supply.reservation.stream.Reserv
* timeouts
*/
class ReservationClient(config: ClientConfiguration) : GraphQLClient() {
+
+ override val apiEndpoint = SupplyApiEndpointProvider.forEnvironment(config.environment ?: ClientEnvironment.PROD)
+
override val graphQLExecutor: GraphQLExecutor = DefaultGraphQLExecutor(
- config.toExpediaGroupClientConfiguration(
- apiEndpoint = SupplyApiEndpointProvider.forEnvironment(
- environment = config.environment ?: ClientEnvironment.PROD
- ),
- )
+ requestExecutor = DefaultRequestExecutor(config, apiEndpoint),
+ serverUrl = apiEndpoint.endpoint
)
/**
diff --git a/examples/build.gradle b/examples/build.gradle
index 6f5f3508..3f5cb978 100644
--- a/examples/build.gradle
+++ b/examples/build.gradle
@@ -1,5 +1,5 @@
dependencies {
- implementation 'org.slf4j:slf4j-simple:2.0.16'
+ runtimeOnly 'ch.qos.logback:logback-classic:1.5.12'
implementation(project(":code"))
}