Untitled
unknown
plain_text
2 years ago
8.8 kB
7
Indexable
import org.http4k.client.ApacheClient import org.http4k.core.Body import org.http4k.core.Filter import org.http4k.core.HttpHandler import org.http4k.core.Method import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status import org.http4k.core.Uri import org.http4k.core.extend import org.http4k.core.then import org.http4k.core.with import org.http4k.filter.ClientFilters import org.http4k.filter.ResponseFilters import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import java.util.UUID interface ApiGwClient { fun subscribeForProjectUpdates(projectId: String): ProjectSubscription fun getProjectUpdateSubscription(projectId: String): ProjectSubscription fun deleteProjectUpdateSubscription(projectId: String): Boolean data class ProjectSubscription( val projectId: String, val callbackURL: String, ) { constructor(projectId: String, callbackURL: Uri) : this(projectId, callbackURL.toString()) } } @Component class EbipClient( apiGwBaseUrl: Uri, private val callbackBaseUrl: Uri, private val apiKey: String, private val apiUser: String, private val tokenUrl: String, private val tokenAuthorization: String, private val tokenRefreshBeforeExpiry: Int ) : ApiGwClient { @Autowired constructor( @Value("\${api.gateway.url}") apiGwBaseUrl: String, @Value("\${eca.integrator.base.url}") callbackBaseUrl: String, @Value("\${api.gateway.key}") apiKey: String, @Value("\${api.gateway.user}") apiUser: String, @Value("\${api.gateway.token.url}") tokenUrl: String, @Value("\${api.gateway.token.authorization}") tokenAuthorization: String, @Value("\${api.gateway.token.refresh}") tokenRefreshBeforeExpiry: Int ) : this( apiGwBaseUrl = Uri.of(apiGwBaseUrl), callbackBaseUrl = Uri.of(callbackBaseUrl), apiKey = apiKey, apiUser = apiUser, tokenUrl = tokenUrl, tokenAuthorization = tokenAuthorization, tokenRefreshBeforeExpiry = tokenRefreshBeforeExpiry ) private val client = ClientFilters.SetBaseUriFrom(apiGwBaseUrl) .then(ResponseFilters.ReportHttpTransaction { tx -> logger.info("{}\n\nduration: {}\n{}", tx.request, tx.duration, tx.response) }) .then(EBIPAuthentication( apiKey = apiKey, apiUser = apiUser, tokenUrl = tokenUrl, tokenAuthorization = tokenAuthorization, tokenRefreshBeforeExpiry = tokenRefreshBeforeExpiry )) .then(ApacheClient()) private val projectSubscriptionsBaseUrl = Uri.of("acceptanceprojecteventsubscription/v1/subscriptions") override fun subscribeForProjectUpdates(projectId: String): ApiGwClient.ProjectSubscription { val subscription = ApiGwClient.ProjectSubscription( projectId = projectId, callbackURL = callbackBaseUrl / "projects/$projectId/update-event" ) return runCatching { client( Request(Method.POST, projectSubscriptionsBaseUrl) .with(Body.auto<ApiGwClient.ProjectSubscription>().toLens() of subscription) ) }.recoverCatching { exception -> throw ApiGwException.ApiException(exception.message!!, exception) }.mapCatching { response -> if (response.status.successful) subscription else throw getErrorFromResponse(response) }.getOrThrow() } override fun getProjectUpdateSubscription(projectId: String): ApiGwClient.ProjectSubscription { val response = client(Request(Method.GET, projectSubscriptionsBaseUrl / projectId)) return if (response.status.successful) { Body.auto<ApiGwClient.ProjectSubscription>().toLens().extract(response) } else { throw getErrorFromResponse(response) } } override fun deleteProjectUpdateSubscription(projectId: String): Boolean { val response = client(Request(Method.DELETE, projectSubscriptionsBaseUrl / projectId)) return when { response.status.successful -> true // Bad implementation on the API GW. When fixed this can be removed response.status == Status.INTERNAL_SERVER_ERROR && response.bodyString().trim() == "Subscription event deleted successfully" -> true else -> throw getErrorFromResponse(response) } } private operator fun Uri.div(path: String): Uri = extend(Uri.of(path)) private operator fun Uri.div(path: UUID): Uri = extend(Uri.of(path.toString())) private fun getErrorFromResponse(response: Response): ApiGwException = runCatching { Body.auto<ErrorInfo>().toLens().extract(response) } .map { errorInfo -> ApiGwException.ApiException( message = "${errorInfo.responseStatusCode} ${errorInfo.responseMessage}", description = errorInfo.responseMessageDescription, traceId = errorInfo.traceId ) }.recover { ApiGwException.ApiException( message = response.status.toString(), description = response.bodyString() ) }.getOrThrow() data class ErrorInfo( val traceId: String?, val responseStatusCode: Int, val responseMessage: String, val responseMessageDescription: String? ) companion object { private val logger = LoggerFactory.getLogger(EbipClient::class.java) } } /** * Filter that will add authentication headers to the request * * Each request to EBIP API Gateway needs the following headers * - "Authorization" * - "x-Gateway-APIKey" * - "x-api-user" */ class EBIPAuthentication( private val apiKey: String, private val apiUser: String, private val tokenUrl: String, private val tokenAuthorization: String, private val tokenRefreshBeforeExpiry: Int ) : Filter { override operator fun invoke(next: HttpHandler): HttpHandler = { request -> val (token, _) = getToken() next(request.headers( listOf( AUTHORIZATION to "Bearer $token", GATEWAY_API_KEY to apiKey, GATEWAY_API_USER to apiUser, ) )) } /** * Get new token, [refreshBeforeExpiry] seconds before expiry * reuse current token if expiry date is further */ private fun getToken( refreshBeforeExpiry: Int = tokenRefreshBeforeExpiry ): TokenWithExpiration { synchronized(tokenUrl) { val currentToken = cachedToken if (currentToken?.isExpired(-refreshBeforeExpiry) == false) return currentToken logger.info("Token expired or missing. Try to obtain new token...") val client = ApacheClient() val request = Request(Method.POST, tokenUrl) .body("grant_type=client_credentials") .headers( listOf( "Authorization" to tokenAuthorization, "Content-Type" to "application/x-www-form-urlencoded" ) ) val result = client(request) if (!result.status.successful) { cachedToken = null throw ApiGwException.TokenGenerationException(result) } val resultJson = CustomJackson.parse(result.bodyString()) val token = resultJson.get("access_token").asText() val expire = resultJson.get("expires_on").asLong() logger.info("Got new token that will expire at $expire") return TokenWithExpiration(token, expire).also { cachedToken = it } } } private var cachedToken: TokenWithExpiration? = null private data class TokenWithExpiration(val token: String, val expiresAt: Long) { /** Check if current token is expired. * * The token is considered valid [tolerance] seconds after [expiresAt]. * If [tolerance] is negative then the token will be considered expired early. */ fun isExpired(tolerance: Int = 0): Boolean = expiresAt + tolerance < System.currentTimeMillis() / 1000 } companion object { private val logger = LoggerFactory.getLogger(EBIPAuthentication::class.java) private const val AUTHORIZATION = "Authorization" private const val GATEWAY_API_KEY = "x-Gateway-APIKey" private const val GATEWAY_API_USER = "x-api-user" } }
Editor is loading...