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")) }