Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: new SDK core integration #114

Merged
merged 21 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c89ea59
feat: lodging connectivity sdk based on google api client library (#63)
OmarAlJarrah Oct 30, 2024
7956316
refactor: use manifest for user agent (#82)
OmarAlJarrah Oct 31, 2024
06c00ce
feat: graphql executor sync and async execute method (#83)
OmarAlJarrah Oct 31, 2024
2c9fc51
refactor: cleaup package versioning and set v2 as default (#84)
OmarAlJarrah Nov 6, 2024
54c5a33
feat: merge latest changes from main to the feature branch (#100)
Mohammad-Dwairi Nov 14, 2024
0720f05
feat: add SDK base HTTP models (#104)
Mohammad-Dwairi Nov 28, 2024
e237320
Merge branch 'main' into mdwairi/injectable-http-client
Mohammad-Dwairi Dec 1, 2024
cedb0a9
feat: cleanup old core package (#110)
Mohammad-Dwairi Dec 1, 2024
459399d
chore: improve bearer auth implementation
Mohammad-Dwairi Dec 1, 2024
82daf2e
chore: minor enhancements and cleanups
Mohammad-Dwairi Dec 1, 2024
ff69424
chore: unify Kotlin plugin configs in the root build.gradle
Mohammad-Dwairi Dec 1, 2024
7362ce2
fix: improve logging implementation
Mohammad-Dwairi Dec 1, 2024
95ddb46
fix: log authentication requests
Mohammad-Dwairi Dec 1, 2024
71662c0
fix: add logback for examples module
Mohammad-Dwairi Dec 1, 2024
4c90863
chore: minor logging improvements
Mohammad-Dwairi Dec 1, 2024
88f7a37
chore: minor logging improvements
Mohammad-Dwairi Dec 2, 2024
185a818
Merge branch 'main' into mdwairi/injectable-http-client
Mohammad-Dwairi Dec 2, 2024
f5a6ec4
chore: remove dummy files
Mohammad-Dwairi Dec 3, 2024
211c352
chore: cleanup ClientConfiguration.kt
Mohammad-Dwairi Dec 3, 2024
2133e65
chore: cleanup Headers.kt
Mohammad-Dwairi Dec 3, 2024
06c1f50
chore: cleanup docs
Mohammad-Dwairi Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
```

Expand All @@ -29,7 +29,7 @@ dependencies {
<dependency>
<groupId>com.expediagroup</groupId>
<artifactId>lodging-connectivity-sdk</artifactId>
<version>1.0.7-SNAPSHOT</version>
<version>1.0.6-SNAPSHOT</version>
</dependency>
```

Expand Down
11 changes: 1 addition & 10 deletions apollo-compiler-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -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
Expand Down
37 changes: 16 additions & 21 deletions code/build.gradle
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 {
Expand All @@ -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"


Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <token>` 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))
}
}
Loading