diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/client/AsyncXapClient.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/client/AsyncXapClient.kt new file mode 100644 index 000000000..4eb562950 --- /dev/null +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/client/AsyncXapClient.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 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.xap.client + +import com.expediagroup.sdk.rest.AsyncRestClient +import com.expediagroup.sdk.rest.AsyncRestExecutor +import com.expediagroup.sdk.rest.model.Response +import com.expediagroup.sdk.rest.trait.operation.JacksonModelOperationResponseBodyTrait +import com.expediagroup.sdk.rest.trait.operation.OperationNoResponseBodyTrait +import com.expediagroup.sdk.xap.configuration.AsyncClientBuilder +import com.expediagroup.sdk.xap.configuration.AsyncXapClientConfiguration +import com.expediagroup.sdk.xap.configuration.Constant.ENDPOINT +import com.expediagroup.sdk.xap.configuration.OBJECT_MAPPER +import com.expediagroup.sdk.xap.core.AsyncRequestExecutor +import java.util.concurrent.CompletableFuture + +/** + * Asynchronous client for XAP API. + * + * @property restExecutor The executor for handling REST operations. + */ +class AsyncXapClient private constructor( + config: AsyncXapClientConfiguration, +) : AsyncRestClient() { + override val restExecutor: AsyncRestExecutor = + AsyncRestExecutor( + mapper = OBJECT_MAPPER, + serverUrl = ENDPOINT, + requestExecutor = AsyncRequestExecutor(configuration = config), + ) + + /** + * Executes an operation that does not expect a response body. + * + * @param operation The operation to execute. + * @return A CompletableFuture containing the response. + */ + fun execute(operation: OperationNoResponseBodyTrait): CompletableFuture> = restExecutor.execute(operation) + + /** + * Executes an operation that expects a response body. + * + * @param T The type of the response body. + * @param operation The operation to execute. + * @return A CompletableFuture containing the response. + */ + fun execute(operation: JacksonModelOperationResponseBodyTrait): CompletableFuture> = restExecutor.execute(operation) + + companion object { + /** + * Builder for creating an instance of [AsyncXapClient]. + */ + class Builder : AsyncClientBuilder() { + override fun build(): AsyncXapClient = AsyncXapClient(buildConfig()) + } + + /** + * Creates a new builder for [AsyncXapClient]. + * + * @return A new [Builder] instance. + */ + @JvmStatic + fun builder() = Builder() + } +} diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/client/XapClient.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/client/XapClient.kt index 0be57e544..9c6d8c208 100644 --- a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/client/XapClient.kt +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/client/XapClient.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Expedia, Inc. + * Copyright (C) 2025 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,355 +13,66 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.expediagroup.sdk.xap.client -import com.expediagroup.sdk.core.client.BaseXapClient -import com.expediagroup.sdk.core.configuration.XapClientConfiguration -import com.expediagroup.sdk.core.constant.ConfigurationName -import com.expediagroup.sdk.core.constant.provider.ExceptionMessageProvider.getMissingRequiredConfigurationMessage -import com.expediagroup.sdk.core.model.EmptyResponse -import com.expediagroup.sdk.core.model.Nothing -import com.expediagroup.sdk.core.model.Operation -import com.expediagroup.sdk.core.model.Response -import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupConfigurationException -import com.expediagroup.sdk.core.model.exception.handle -import com.expediagroup.sdk.xap.models.* -import com.expediagroup.sdk.xap.models.exception.ErrorObjectMapper -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiAPIGatewayErrorException -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiAPIMErrorException -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiActivitiesErrorsException -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiCarsErrorsException -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiErrorsException -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiLodgingErrorsException -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiPresignedUrlResponseException -import com.expediagroup.sdk.xap.models.exception.ExpediaGroupApiSdpAPIMErrorException -import com.expediagroup.sdk.xap.operations.GetActivityDetailsOperation -import com.expediagroup.sdk.xap.operations.GetActivityListingsOperation -import com.expediagroup.sdk.xap.operations.GetCarDetailsOperation -import com.expediagroup.sdk.xap.operations.GetCarsListingsOperation -import com.expediagroup.sdk.xap.operations.GetFeedDownloadUrlOperation -import com.expediagroup.sdk.xap.operations.GetLodgingAvailabilityCalendarsOperation -import com.expediagroup.sdk.xap.operations.GetLodgingDetailsOperation -import com.expediagroup.sdk.xap.operations.GetLodgingListingsOperation -import com.expediagroup.sdk.xap.operations.GetLodgingQuotesOperation -import com.expediagroup.sdk.xap.operations.GetLodgingRateCalendarOperation -import io.ktor.client.call.body -import io.ktor.client.request.request -import io.ktor.client.request.setBody -import io.ktor.client.request.url -import io.ktor.client.statement.HttpResponse -import io.ktor.http.ContentType -import io.ktor.http.HttpMethod -import io.ktor.http.contentType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.future.future -import java.util.concurrent.CompletableFuture +import com.expediagroup.sdk.rest.RestClient +import com.expediagroup.sdk.rest.RestExecutor +import com.expediagroup.sdk.rest.model.Response +import com.expediagroup.sdk.rest.trait.operation.JacksonModelOperationResponseBodyTrait +import com.expediagroup.sdk.rest.trait.operation.OperationNoResponseBodyTrait +import com.expediagroup.sdk.xap.configuration.ClientBuilder +import com.expediagroup.sdk.xap.configuration.Constant.ENDPOINT +import com.expediagroup.sdk.xap.configuration.OBJECT_MAPPER +import com.expediagroup.sdk.xap.configuration.XapClientConfiguration +import com.expediagroup.sdk.xap.core.RequestExecutor /** -* The XAP Lodging Search APIs can be used by partners both booking via an Expedia website, or by partners that -will be booking via the XAP APIs. Each API also provides pre-configured links to the Expedia website, -the XAP Booking API, or both. - -*/ - -class XapClient private constructor(clientConfiguration: XapClientConfiguration) : BaseXapClient("xap", clientConfiguration) { - class Builder : BaseXapClient.Builder() { - override fun build() = - XapClient( - XapClientConfiguration(key, secret, endpoint, requestTimeout, connectionTimeout, socketTimeout, maskedLoggingHeaders, maskedLoggingBodyFields) - ) - } - - class BuilderWithHttpClient() : BaseXapClient.BuilderWithHttpClient() { - override fun build(): XapClient { - if (okHttpClient == null) { - throw ExpediaGroupConfigurationException(getMissingRequiredConfigurationMessage(ConfigurationName.OKHTTP_CLIENT)) - } - - return XapClient( - XapClientConfiguration(key, secret, endpoint, null, null, null, maskedLoggingHeaders, maskedLoggingBodyFields, okHttpClient) - ) - } - } - - companion object { - @JvmStatic fun builder() = Builder() - - @JvmStatic fun builderWithHttpClient() = BuilderWithHttpClient() - } - - override suspend fun throwServiceException( - response: HttpResponse, - operationId: String - ): Unit = throw ErrorObjectMapper.process(response, operationId) - - private suspend inline fun executeHttpRequest(operation: Operation): HttpResponse = - httpClient.request { - method = HttpMethod.parse(operation.method) - url(operation.url) - - operation.params?.getHeaders()?.let { - headers.appendAll(it) - } - - operation.params?.getQueryParams()?.let { - url.parameters.appendAll(it) - } - - val extraHeaders = - buildMap { - put("key", configurationProvider.key ?: "") - } - - appendHeaders(extraHeaders) - contentType(ContentType.Application.Json) - setBody(operation.requestBody) - } - - private inline fun executeWithEmptyResponse(operation: Operation): EmptyResponse { - try { - return executeAsyncWithEmptyResponse(operation).get() - } catch (exception: Exception) { - exception.handle() - } - } - - private inline fun executeAsyncWithEmptyResponse(operation: Operation): CompletableFuture = - GlobalScope.future(Dispatchers.IO) { - try { - val response = executeHttpRequest(operation) - throwIfError(response, operation.operationId) - EmptyResponse(response.status.value, response.headers.entries()) - } catch (exception: Exception) { - exception.handle() - } - } - - private inline fun execute(operation: Operation): Response { - try { - return executeAsync(operation).get() - } catch (exception: Exception) { - exception.handle() - } - } - - private inline fun executeAsync(operation: Operation): CompletableFuture> = - GlobalScope.future(Dispatchers.IO) { - try { - val response = executeHttpRequest(operation) - throwIfError(response, operation.operationId) - Response(response.status.value, response.body(), response.headers.entries()) - } catch (exception: Exception) { - exception.handle() - } - } - - /** - * - * The Activity Details API provides detailed information about one selected activity. - * @param operation [GetActivityDetailsOperation] - * @throws ExpediaGroupApiActivitiesErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [Response] object with a body of type ActivityDetailsResponse - */ - fun execute(operation: GetActivityDetailsOperation): Response = execute(operation) - - /** - * - * The Activity Details API provides detailed information about one selected activity. - * @param operation [GetActivityDetailsOperation] - * @throws ExpediaGroupApiActivitiesErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [CompletableFuture] object with a body of type ActivityDetailsResponse - */ - fun executeAsync(operation: GetActivityDetailsOperation): CompletableFuture> = executeAsync(operation) - - /** - * - * The Activities Search API allows partners to search for Expedia Activity inventory. - * @param operation [GetActivityListingsOperation] - * @throws ExpediaGroupApiActivitiesErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [Response] object with a body of type ActivityListingsResponse - */ - fun execute(operation: GetActivityListingsOperation): Response = execute(operation) - - /** - * - * The Activities Search API allows partners to search for Expedia Activity inventory. - * @param operation [GetActivityListingsOperation] - * @throws ExpediaGroupApiActivitiesErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [CompletableFuture] object with a body of type ActivityListingsResponse - */ - fun executeAsync(operation: GetActivityListingsOperation): CompletableFuture> = executeAsync(operation) - - /** - * Get Extended information with a single car offer - * Extended information about the rates, charges, fees, and other terms associated with a single car offer. - * @param operation [GetCarDetailsOperation] - * @throws ExpediaGroupApiCarsErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [Response] object with a body of type CarDetailsResponse - */ - fun execute(operation: GetCarDetailsOperation): Response = execute(operation) - - /** - * Get Extended information with a single car offer - * Extended information about the rates, charges, fees, and other terms associated with a single car offer. - * @param operation [GetCarDetailsOperation] - * @throws ExpediaGroupApiCarsErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [CompletableFuture] object with a body of type CarDetailsResponse - */ - fun executeAsync(operation: GetCarDetailsOperation): CompletableFuture> = executeAsync(operation) - - /** - * Search Expedia car inventory - * Search Expedia car inventory by date, pickup, and dropoff location to return a listing of available cars for hire. - * @param operation [GetCarsListingsOperation] - * @throws ExpediaGroupApiCarsErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [Response] object with a body of type CarListingsResponse - */ - fun execute(operation: GetCarsListingsOperation): Response = execute(operation) - - /** - * Search Expedia car inventory - * Search Expedia car inventory by date, pickup, and dropoff location to return a listing of available cars for hire. - * @param operation [GetCarsListingsOperation] - * @throws ExpediaGroupApiCarsErrorsException - * @throws ExpediaGroupApiAPIMErrorException - * @throws ExpediaGroupApiException - * @return a [CompletableFuture] object with a body of type CarListingsResponse - */ - fun executeAsync(operation: GetCarsListingsOperation): CompletableFuture> = executeAsync(operation) - - /** + * Synchronous client for XAP API. + * + * @property restExecutor The executor for handling REST operations. + */ +class XapClient private constructor( + config: XapClientConfiguration, +) : RestClient() { + override val restExecutor: RestExecutor = + RestExecutor( + mapper = OBJECT_MAPPER, + serverUrl = ENDPOINT, + requestExecutor = RequestExecutor(config), + ) + + /** + * Executes an operation that does not expect a response body. * - * Get the Download URL and other details of the static files. - * @param operation [GetFeedDownloadUrlOperation] - * @throws ExpediaGroupApiPresignedUrlResponseException - * @throws ExpediaGroupApiSdpAPIMErrorException - * @return a [Response] object with a body of type PresignedUrlResponse + * @param operation The operation to execute. + * @return The response of the operation. */ - fun execute(operation: GetFeedDownloadUrlOperation): Response = execute(operation) + fun execute(operation: OperationNoResponseBodyTrait): Response = restExecutor.execute(operation) /** + * Executes an operation that expects a response body. * - * Get the Download URL and other details of the static files. - * @param operation [GetFeedDownloadUrlOperation] - * @throws ExpediaGroupApiPresignedUrlResponseException - * @throws ExpediaGroupApiSdpAPIMErrorException - * @return a [CompletableFuture] object with a body of type PresignedUrlResponse - */ - fun executeAsync(operation: GetFeedDownloadUrlOperation): CompletableFuture> = executeAsync(operation) - - /** - * Get availability calendars of properties - * Returns the availability of each day for a range of dates for given Expedia lodging properties. - * @param operation [GetLodgingAvailabilityCalendarsOperation] - * @throws ExpediaGroupApiLodgingErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [Response] object with a body of type AvailabilityCalendarResponse - */ - fun execute(operation: GetLodgingAvailabilityCalendarsOperation): Response = execute(operation) - - /** - * Get availability calendars of properties - * Returns the availability of each day for a range of dates for given Expedia lodging properties. - * @param operation [GetLodgingAvailabilityCalendarsOperation] - * @throws ExpediaGroupApiLodgingErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [CompletableFuture] object with a body of type AvailabilityCalendarResponse - */ - fun executeAsync(operation: GetLodgingAvailabilityCalendarsOperation): CompletableFuture> = executeAsync(operation) - - /** - * Get Extended information with a single property offer - * Extended information about the rate, charges, fees, and financial terms associated with booking a single lodging rate plan offer. - * @param operation [GetLodgingDetailsOperation] - * @throws ExpediaGroupApiErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [Response] object with a body of type HotelDetailsResponse - */ - fun execute(operation: GetLodgingDetailsOperation): Response = execute(operation) - - /** - * Get Extended information with a single property offer - * Extended information about the rate, charges, fees, and financial terms associated with booking a single lodging rate plan offer. - * @param operation [GetLodgingDetailsOperation] - * @throws ExpediaGroupApiErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [CompletableFuture] object with a body of type HotelDetailsResponse - */ - fun executeAsync(operation: GetLodgingDetailsOperation): CompletableFuture> = executeAsync(operation) - - /** - * Search lodging inventory - * Search Expedia lodging inventory by Location Keyword, Region ID, Lat/Long, or Hotel ID(s) and return up to 1,000 offers in response. Provides deeplink to Expedia site to book, or rate plan info to enable API booking. - * @param operation [GetLodgingListingsOperation] - * @throws ExpediaGroupApiErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [Response] object with a body of type HotelListingsResponse - */ - fun execute(operation: GetLodgingListingsOperation): Response = execute(operation) - - /** - * Search lodging inventory - * Search Expedia lodging inventory by Location Keyword, Region ID, Lat/Long, or Hotel ID(s) and return up to 1,000 offers in response. Provides deeplink to Expedia site to book, or rate plan info to enable API booking. - * @param operation [GetLodgingListingsOperation] - * @throws ExpediaGroupApiErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [CompletableFuture] object with a body of type HotelListingsResponse + * @param T The type of the response body. + * @param operation The operation to execute. + * @return The response of the operation. */ - fun executeAsync(operation: GetLodgingListingsOperation): CompletableFuture> = executeAsync(operation) + fun execute(operation: JacksonModelOperationResponseBodyTrait): Response = restExecutor.execute(operation) - /** - * Get properties price and availability information - * The Lodging Quotes API will return the price and availability information for given Expedia lodging property ID(s). - * @param operation [GetLodgingQuotesOperation] - * @throws ExpediaGroupApiLodgingErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [Response] object with a body of type LodgingQuotesResponse - */ - fun execute(operation: GetLodgingQuotesOperation): Response = execute(operation) - - /** - * Get properties price and availability information - * The Lodging Quotes API will return the price and availability information for given Expedia lodging property ID(s). - * @param operation [GetLodgingQuotesOperation] - * @throws ExpediaGroupApiLodgingErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [CompletableFuture] object with a body of type LodgingQuotesResponse - */ - fun executeAsync(operation: GetLodgingQuotesOperation): CompletableFuture> = executeAsync(operation) - - /** - * Get rate calendar of a property - * The Rate Calendar API will return the lowest rate plan for a range of days for one selected Expedia lodging property. - * @param operation [GetLodgingRateCalendarOperation] - * @throws ExpediaGroupApiErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [Response] object with a body of type RateCalendarResponse - */ - fun execute(operation: GetLodgingRateCalendarOperation): Response = execute(operation) + companion object { + /** + * Builder for creating an instance of [XapClient]. + */ + class Builder : ClientBuilder() { + override fun build(): XapClient = XapClient(buildConfig()) + } - /** - * Get rate calendar of a property - * The Rate Calendar API will return the lowest rate plan for a range of days for one selected Expedia lodging property. - * @param operation [GetLodgingRateCalendarOperation] - * @throws ExpediaGroupApiErrorsException - * @throws ExpediaGroupApiAPIGatewayErrorException - * @return a [CompletableFuture] object with a body of type RateCalendarResponse - */ - fun executeAsync(operation: GetLodgingRateCalendarOperation): CompletableFuture> = executeAsync(operation) + /** + * Creates a new builder for [XapClient]. + * + * @return A new [Builder] instance. + */ + @JvmStatic + fun builder() = Builder() + } } diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/Constant.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/Constant.kt new file mode 100644 index 000000000..f70e720d8 --- /dev/null +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/Constant.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 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.xap.configuration + +internal object Constant { + const val ENDPOINT = "https://apim.expedia.com" + const val AUTH_ENDPOINT = "https://api.expediagroup.com/identity/oauth2/v3/token" +} diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/XapClientConfiguration.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/XapClientConfiguration.kt new file mode 100644 index 000000000..caaaafc2a --- /dev/null +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/XapClientConfiguration.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2025 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.xap.configuration + +import com.expediagroup.sdk.core.auth.common.Credentials +import com.expediagroup.sdk.core.transport.AsyncTransport +import com.expediagroup.sdk.core.transport.Transport +import com.expediagroup.sdk.rest.AsyncRestClient +import com.expediagroup.sdk.rest.RestClient + +/** + * Configuration data class for XAP client. + * + * @property transport The transport mechanism. Defaults to null. + */ +data class XapClientConfiguration( + val credentials: Credentials, + val transport: Transport? = null, +) + +/** + * Configuration data class for asynchronous XAP client. + * + * @property asyncTransport The asynchronous transport mechanism. Defaults to null. + */ +data class AsyncXapClientConfiguration( + val credentials: Credentials, + val asyncTransport: AsyncTransport? = null, +) + +/** + * Abstract builder class for creating instances of [RestClient]. + * + * @param T The type of [RestClient] to build. + */ +abstract class ClientBuilder { + private var credentials: Credentials? = null + private var transport: Transport? = null + + /** + * Sets the credentials used to authenticate with the API. + * + * @param credentials + * @return The builder instance. + */ + fun credentials(credentials: Credentials) = apply { this.credentials = credentials } + + /** + * Sets the transport mechanism. + * + * @param transport The transport mechanism. + * @return The builder instance. + */ + fun transport(transport: Transport) = apply { this.transport = transport } + + /** + * Builds the [RestClient] instance. + * + * @return The built [RestClient] instance. + */ + abstract fun build(): T + + /** + * Builds the configuration for the XAP client. + * + * @return The built [XapClientConfiguration] instance. + * @throws IllegalArgumentException If the credentials type is unsupported + */ + protected fun buildConfig(): XapClientConfiguration { + requireNotNull(credentials) { "credentials is required" } + + return XapClientConfiguration( + credentials = credentials!!, + transport = transport, + ) + } +} + +/** + * Abstract builder class for creating instances of [AsyncRestClient]. + * + * @param T The type of [AsyncRestClient] to build. + */ +abstract class AsyncClientBuilder { + private var credentials: Credentials? = null + private var asyncTransport: AsyncTransport? = null + + /** + * Sets the credentials used to authenticate with the API. + * + * @param credentials + * @return The builder instance. + */ + fun credentials(credentials: Credentials) = apply { this.credentials = credentials } + + /** + * Sets the asynchronous transport mechanism. + * + * @param asyncTransport The asynchronous transport mechanism. + * @return The builder instance. + */ + fun asyncTransport(asyncTransport: AsyncTransport) = apply { this.asyncTransport = asyncTransport } + + /** + * Builds the [AsyncRestClient] instance. + * + * @return The built [AsyncRestClient] instance. + */ + abstract fun build(): T + + /** + * Builds the configuration for the asynchronous XAP client. + * + * @return The built [AsyncXapClientConfiguration] instance. + * @throws IllegalArgumentException If the credentials type is unsupported + */ + protected fun buildConfig(): AsyncXapClientConfiguration { + requireNotNull(credentials) { + "credentials is required" + } + + return AsyncXapClientConfiguration( + credentials = credentials!!, + asyncTransport = asyncTransport, + ) + } +} diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/XapJacksonObjectMapper.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/XapJacksonObjectMapper.kt new file mode 100644 index 000000000..c08df59cf --- /dev/null +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/configuration/XapJacksonObjectMapper.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 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.xap.configuration + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder + +/** + * Singleton instance of [ObjectMapper] configured for XAP. + * + * This instance is built using `jacksonMapperBuilder` and automatically registers all available modules. + */ +val OBJECT_MAPPER: ObjectMapper = + jacksonMapperBuilder() + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build() + .findAndRegisterModules() diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/ApiKeyHeaderStep.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/ApiKeyHeaderStep.kt new file mode 100644 index 000000000..58326ff93 --- /dev/null +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/ApiKeyHeaderStep.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 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.xap.core + +import com.expediagroup.sdk.core.http.Request +import com.expediagroup.sdk.core.pipeline.RequestPipelineStep + +/** + * A pipeline step that adds an API key header to the request. + * + * @property apiKey The API key to be added to the request header. + */ +class ApiKeyHeaderStep( + private val apiKey: String, +) : RequestPipelineStep { + /** + * Adds the API key header to the request. + * + * @param request The original request. + * @return The modified request with the API key header added. + */ + override fun invoke(request: Request): Request = + request + .newBuilder() + .addHeader("key", apiKey) + .build() +} diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/AsyncRequestExecutor.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/AsyncRequestExecutor.kt new file mode 100644 index 000000000..5c809bc1d --- /dev/null +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/AsyncRequestExecutor.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 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.xap.core + +import com.expediagroup.sdk.core.auth.basic.BasicAuthCredentials +import com.expediagroup.sdk.core.auth.basic.BasicAuthManager +import com.expediagroup.sdk.core.auth.oauth.OAuthAsyncManager +import com.expediagroup.sdk.core.auth.oauth.OAuthCredentials +import com.expediagroup.sdk.core.exception.client.ExpediaGroupConfigurationException +import com.expediagroup.sdk.core.logging.LoggerDecorator +import com.expediagroup.sdk.core.logging.masking.MaskHeaders +import com.expediagroup.sdk.core.pipeline.ExecutionPipeline +import com.expediagroup.sdk.core.pipeline.RequestPipelineStep +import com.expediagroup.sdk.core.pipeline.ResponsePipelineStep +import com.expediagroup.sdk.core.pipeline.step.BasicAuthStep +import com.expediagroup.sdk.core.pipeline.step.OAuthStep +import com.expediagroup.sdk.core.pipeline.step.RequestHeadersStep +import com.expediagroup.sdk.core.pipeline.step.RequestLoggingStep +import com.expediagroup.sdk.core.pipeline.step.ResponseLoggingStep +import com.expediagroup.sdk.core.transport.AbstractAsyncRequestExecutor +import com.expediagroup.sdk.xap.configuration.AsyncXapClientConfiguration +import com.expediagroup.sdk.xap.configuration.Constant.AUTH_ENDPOINT +import org.slf4j.LoggerFactory + +/** + * Executor for handling asynchronous requests with XAP client configuration. + * + * @param configuration The configuration for the asynchronous XAP client. + */ +class AsyncRequestExecutor( + private val configuration: AsyncXapClientConfiguration, +) : AbstractAsyncRequestExecutor(configuration.asyncTransport) { + private val headersMask = MaskHeaders(listOf("authorization")) + + override val executionPipeline = + ExecutionPipeline( + requestPipeline = getRequestPipeline(), + responsePipeline = getResponsePipeline(), + ) + + private fun getRequestPipeline(): List = + when (configuration.credentials) { + is BasicAuthCredentials -> + listOf( + RequestHeadersStep(), + ApiKeyHeaderStep(configuration.credentials.username), + BasicAuthStep(BasicAuthManager(configuration.credentials)), + RequestLoggingStep( + logger = logger, + maskHeaders = headersMask, + ), + ) + + is OAuthCredentials -> + listOf( + RequestHeadersStep(), + ApiKeyHeaderStep(configuration.credentials.key), + OAuthStep( + OAuthAsyncManager( + credentials = configuration.credentials, + asyncTransport = asyncTransport, + authUrl = AUTH_ENDPOINT, + ), + ), + RequestLoggingStep( + logger = logger, + maskHeaders = headersMask, + ), + ) + + else -> throw ExpediaGroupConfigurationException("Unsupported credentials type: ${configuration.credentials.javaClass.name}") + } + + private fun getResponsePipeline(): List = + listOf( + ResponseLoggingStep(logger), + ) + + companion object { + private val logger = LoggerDecorator(LoggerFactory.getLogger(this::class.java.enclosingClass)) + } +} diff --git a/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/RequestExecutor.kt b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/RequestExecutor.kt new file mode 100644 index 000000000..0d989ebc6 --- /dev/null +++ b/xap-sdk/src/main/kotlin/com/expediagroup/sdk/xap/core/RequestExecutor.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 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.xap.core + +import com.expediagroup.sdk.core.auth.basic.BasicAuthCredentials +import com.expediagroup.sdk.core.auth.basic.BasicAuthManager +import com.expediagroup.sdk.core.auth.oauth.OAuthCredentials +import com.expediagroup.sdk.core.auth.oauth.OAuthManager +import com.expediagroup.sdk.core.exception.client.ExpediaGroupConfigurationException +import com.expediagroup.sdk.core.logging.LoggerDecorator +import com.expediagroup.sdk.core.logging.masking.MaskHeaders +import com.expediagroup.sdk.core.pipeline.ExecutionPipeline +import com.expediagroup.sdk.core.pipeline.RequestPipelineStep +import com.expediagroup.sdk.core.pipeline.ResponsePipelineStep +import com.expediagroup.sdk.core.pipeline.step.BasicAuthStep +import com.expediagroup.sdk.core.pipeline.step.OAuthStep +import com.expediagroup.sdk.core.pipeline.step.RequestHeadersStep +import com.expediagroup.sdk.core.pipeline.step.RequestLoggingStep +import com.expediagroup.sdk.core.pipeline.step.ResponseLoggingStep +import com.expediagroup.sdk.core.transport.AbstractRequestExecutor +import com.expediagroup.sdk.xap.configuration.Constant.AUTH_ENDPOINT +import com.expediagroup.sdk.xap.configuration.XapClientConfiguration +import org.slf4j.LoggerFactory + +/** + * Executor for handling synchronous requests with XAP client configuration. + * + * @param configuration The configuration for the XAP client. + */ +class RequestExecutor( + private val configuration: XapClientConfiguration, +) : AbstractRequestExecutor(configuration.transport) { + private val headersMask = MaskHeaders(listOf("authorization")) + + override val executionPipeline = + ExecutionPipeline( + requestPipeline = getRequestPipeline(), + responsePipeline = getResponsePipeline(), + ) + + private fun getRequestPipeline(): List = + when (configuration.credentials) { + is BasicAuthCredentials -> + listOf( + RequestHeadersStep(), + ApiKeyHeaderStep(configuration.credentials.username), + BasicAuthStep(BasicAuthManager(configuration.credentials)), + RequestLoggingStep( + logger = logger, + maskHeaders = headersMask, + ), + ) + + is OAuthCredentials -> + listOf( + RequestHeadersStep(), + ApiKeyHeaderStep(configuration.credentials.key), + OAuthStep( + OAuthManager( + credentials = configuration.credentials, + transport = transport, + authUrl = AUTH_ENDPOINT, + ), + ), + RequestLoggingStep( + logger = logger, + maskHeaders = headersMask, + ), + ) + + else -> throw ExpediaGroupConfigurationException("Unsupported credentials type: ${configuration.credentials.javaClass.name}") + } + + private fun getResponsePipeline(): List = + listOf( + ResponseLoggingStep(logger), + ) + + companion object { + private val logger = LoggerDecorator(LoggerFactory.getLogger(this::class.java.enclosingClass)) + } +}