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: extract the core package to a separate Gradle module #151

Merged
merged 16 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/workflows/code-quality-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,3 @@ jobs:
- name: Run Checks
id: build
run: gradle clean build
- name: Upload Coverage Report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: code/build/reports/kover
Mohammad-Dwairi marked this conversation as resolved.
Show resolved Hide resolved
89 changes: 89 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import kotlinx.kover.gradle.plugin.dsl.AggregationType
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit

plugins {
id 'org.jetbrains.kotlin.jvm' version '2.1.0'

/* Test Reporting */
id 'org.jetbrains.kotlinx.kover' version "0.9.0"

/* Linting */
id "org.jlleitschuh.gradle.ktlint" version "12.1.2"
}

kotlin {
jvmToolchain(8)
}

dependencies {
/* Kotlin */
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.1.0'

/* EG SDK Core */
implementation 'com.squareup.okio:okio:3.10.2'
implementation 'com.ebay.ejmask:ejmask-api:1.3.0'
implementation 'com.ebay.ejmask:ejmask-extensions:1.3.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2'
implementation 'org.slf4j:slf4j-api:2.0.16'

/* Testing */
testImplementation platform('org.junit:junit-bom:5.11.4')
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'io.mockk:mockk:1.13.14'
testImplementation "com.squareup.okhttp3:mockwebserver:4.12.0"
}

ktlint {
debug = true
version = "1.5.0"
verbose = true

additionalEditorconfig = [
"max_line_length": "200",
"indent_style": "space",
"indent_size": "4",
"insert_final_newline": "true",
"end_of_line": "lf",
]
}

test {
useJUnitPlatform()

testLogging {
events "passed", "skipped", "failed"
}

classpath = classpath.filter {
// exclude original sdk.properties file during testing
!it.absolutePath.contains("build/resources/main/sdk.properties")
}
}

tasks.named("check") {
finalizedBy("koverHtmlReport")
}

kover {
reports {
total {
verify {
rule {
bound {
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
coverageUnits = CoverageUnit.LINE
minValue = 92
}
bound {
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
coverageUnits = CoverageUnit.BRANCH
minValue = 86
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.http.CommonMediaTypes
import com.expediagroup.sdk.core.http.Method
import com.expediagroup.sdk.core.http.Request
import com.expediagroup.sdk.core.http.RequestBody

/**
* Abstract class that contains common functionalities to store and renew bearer tokens.
* This class does not handle the bearer token fetching or parsing. Subclasses have to implement the
* [AuthenticationManager.authenticate] method where the token is fetched and parsed.
*/
abstract class AbstractBearerAuthenticationManager(
private val authUrl: String,
protected val credentials: Credentials,
) : AuthenticationManager {
@Volatile
private var bearerTokenStorage = BearerTokenStorage.empty

/**
* 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.
*/
fun buildAuthenticationRequest(): Request =
run {
Request
.Builder()
.url(authUrl)
.method(Method.POST)
.body(
RequestBody
.create(mapOf("grant_type" to "client_credentials")),
).setHeader("Authorization", credentials.encodeBasic())
.setHeader("Content-Type", CommonMediaTypes.APPLICATION_FORM_URLENCODED.toString())
.build()
}

/**
* Stores the retrieved token in internal storage for subsequent use.
*
* @param bearerTokenResponse The [BearerTokenResponse] containing the token and its expiration time.
*/
fun storeToken(bearerTokenResponse: BearerTokenResponse) =
run {
bearerTokenStorage =
BearerTokenStorage.create(
accessToken = bearerTokenResponse.accessToken,
expiresIn = bearerTokenResponse.expiresIn,
)
}
}
Original file line number Diff line number Diff line change
@@ -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.authentication.bearer

import com.expediagroup.sdk.core.authentication.common.Credentials
import com.expediagroup.sdk.core.exception.service.ExpediaGroupAuthException
import com.expediagroup.sdk.core.http.Request
import com.expediagroup.sdk.core.http.Response
import com.expediagroup.sdk.core.logging.LoggerDecorator
import com.expediagroup.sdk.core.pipeline.ExecutionPipeline
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.core.transport.AsyncTransport
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletableFuture

/**
* Manages bearer tokens fetching, parsing, storing, and renewal.
* This async implementation uses the injected [AsyncTransport] to make the authentication requests.
* Typically, this async implementation should be used with async SDK calls where the [AsyncTransport] is already
* configured and used to handle requests.
*/
class BearerAuthenticationAsyncManager(
authUrl: String,
credentials: Credentials,
private val asyncTransport: AsyncTransport,
) : AbstractBearerAuthenticationManager(authUrl, credentials) {
private val requestExecutor =
object : AbstractAsyncRequestExecutor(asyncTransport) {
override val executionPipeline =
ExecutionPipeline(
requestPipeline =
listOf(
RequestHeadersStep(),
RequestLoggingStep(logger),
),
responsePipeline =
listOf(
ResponseLoggingStep(logger),
),
)
}

/**
* 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.
*/
override fun authenticate() {
try {
clearAuthentication()
executeAuthenticationRequest(buildAuthenticationRequest())
.thenApply { BearerTokenResponse.parse(it) }
.thenAccept { storeToken(it) }
.join()
} catch (e: Exception) {
throw ExpediaGroupAuthException(message = "Authentication Failed", cause = e)
}
}

/**
* Executes the authentication request and validates the response.
*/
private fun executeAuthenticationRequest(request: Request): CompletableFuture<Response> =
requestExecutor
.execute(request)
.thenApply {
if (it.isSuccessful) {
it
} else {
throw ExpediaGroupAuthException("Received unsuccessful authentication response: [${it.status}]")
}
}.exceptionally {
throw it
}

companion object {
private val logger = LoggerDecorator(LoggerFactory.getLogger(this::class.java.enclosingClass))
}
}
Loading
Loading