Skip to content

Commit

Permalink
feat: add export template api
Browse files Browse the repository at this point in the history
  • Loading branch information
tomwwinter committed Sep 5, 2024
1 parent dfda718 commit 916752e
Show file tree
Hide file tree
Showing 16 changed files with 942 additions and 4 deletions.
4 changes: 2 additions & 2 deletions application/aam-backend-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ dependencies {
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2")
testImplementation("net.joshka:junit-json-params:5.10.2-r0")
testImplementation("org.eclipse.parsson:parsson:1.1.1")
testImplementation("org.eclipse.parsson:parsson:1.1.7")

testImplementation("io.projectreactor:reactor-test")

Expand All @@ -78,7 +78,7 @@ dependencies {
}
}
testImplementation("org.testcontainers:rabbitmq:1.19.7")
testImplementation("com.github.dasniko:testcontainers-keycloak:3.3.0") {
testImplementation("com.github.dasniko:testcontainers-keycloak:3.4.0") {
constraints {
testImplementation("org.apache.james:apache-mime4j-core:0.8.11") {
because("previous versions have security issues")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.aamdigital.aambackendservice.auth.core

import reactor.core.publisher.Mono

data class TokenResponse(val token: String)

data class AuthConfig(
val clientId: String,
val clientSecret: String,
val tokenEndpoint: String,
val grantType: String,
val scope: String,
)

interface AuthProvider {
fun fetchToken(authClientConfig: AuthConfig): Mono<TokenResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.aamdigital.aambackendservice.auth.core

import com.aamdigital.aambackendservice.error.ExternalSystemException
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import org.springframework.http.MediaType
import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono

data class KeycloakTokenResponse(
@JsonProperty("access_token") val accessToken: String,
)

class KeycloakAuthProvider(
val webClient: WebClient,
val objectMapper: ObjectMapper,
) : AuthProvider {

private val logger = LoggerFactory.getLogger(javaClass)

override fun fetchToken(authClientConfig: AuthConfig): Mono<TokenResponse> {
val formData = LinkedMultiValueMap(
mutableMapOf(
"client_id" to listOf(authClientConfig.clientId),
"client_secret" to listOf(authClientConfig.clientSecret),
"grant_type" to listOf(authClientConfig.grantType),
).also {
if (authClientConfig.scope.isNotBlank()) {
"scope" to listOf(authClientConfig.scope)
}
}
)

return webClient.post()
.uri(authClientConfig.tokenEndpoint)
.headers {
it.contentType = MediaType.APPLICATION_FORM_URLENCODED
}
.body(
BodyInserters.fromFormData(
formData
)
).exchangeToMono {
it.bodyToMono(String::class.java)
}.map {
parseResponse(it)
}.doOnError { logger.error(it.message, it) }
}

private fun parseResponse(raw: String): TokenResponse {
try {
val keycloakTokenResponse = objectMapper.readValue(raw, KeycloakTokenResponse::class.java)
return TokenResponse(
token = keycloakTokenResponse.accessToken
)
} catch (e: Exception) {
throw ExternalSystemException("Could not parse access token from KeycloakAuthProvider", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.aamdigital.aambackendservice.auth.di

import com.aamdigital.aambackendservice.auth.core.AuthProvider
import com.aamdigital.aambackendservice.auth.core.KeycloakAuthProvider
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.WebClient
import reactor.netty.http.client.HttpClient


@ConfigurationProperties("aam-keycloak-client-configuration")
data class KeycloakConfiguration(
val maxInMemorySizeInMegaBytes: Int = 16,
)

@Configuration
class AuthConfiguration {
companion object {
const val MEGA_BYTES_MULTIPLIER = 1024 * 1024
}

@Bean(name = ["aam-keycloak-client"])
fun aamKeycloakWebClient(
configuration: KeycloakConfiguration,
): WebClient {
val clientBuilder =
WebClient.builder()
.codecs {
it.defaultCodecs()
.maxInMemorySize(configuration.maxInMemorySizeInMegaBytes * MEGA_BYTES_MULTIPLIER)
}

return clientBuilder.clientConnector(ReactorClientHttpConnector(HttpClient.create())).build()
}

@Bean(name = ["aam-keycloak"])
fun aamKeycloakAuthProvider(
@Qualifier("aam-keycloak-client") webClient: WebClient,
objectMapper: ObjectMapper,
): AuthProvider =
KeycloakAuthProvider(webClient = webClient, objectMapper = objectMapper)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.aamdigital.aambackendservice.export.controller

import com.aamdigital.aambackendservice.domain.DomainReference
import com.aamdigital.aambackendservice.export.core.CreateTemplateRequest
import com.aamdigital.aambackendservice.export.core.CreateTemplateUseCase
import com.aamdigital.aambackendservice.export.core.RenderTemplateUseCase
import com.fasterxml.jackson.databind.JsonNode
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.MediaType
import org.springframework.http.codec.multipart.FilePart
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToMono
import reactor.core.publisher.Mono


/**
* @param templateId The external identifier of the implementing TemplateEngine
*/
data class CreateTemplateResponseDto(
val templateId: String,
)

@RestController
@RequestMapping("/v1/export")
@Validated
class TemplateExportController(
@Qualifier("aam-render-api-client") val webClient: WebClient,
val createTemplateUseCase: CreateTemplateUseCase,
val renderTemplateUseCase: RenderTemplateUseCase,
) {

@GetMapping("/status")
fun getStatus(): Mono<String> {
return webClient.get().uri("/status").exchangeToMono {
it.bodyToMono()
}
}

@PostMapping("/template")
fun postTemplate(
@RequestPart("template") file: FilePart
): Mono<CreateTemplateResponseDto> {
return createTemplateUseCase.createTemplate(
CreateTemplateRequest(
file = file
)
).map { createTemplateResponse ->
CreateTemplateResponseDto(
templateId = createTemplateResponse.template.id
)
}
}

@PostMapping("/render/{templateId}", produces = [MediaType.APPLICATION_PDF_VALUE])
fun getTemplate(
@PathVariable templateId: String,
@RequestBody templateData: JsonNode,
): Mono<DataBuffer> {
return renderTemplateUseCase.renderTemplate(
templateRef = DomainReference(templateId),
bodyData = templateData
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.aamdigital.aambackendservice.export.core

import com.aamdigital.aambackendservice.domain.DomainReference
import org.springframework.http.codec.multipart.FilePart
import reactor.core.publisher.Mono

data class CreateTemplateResponse(
val template: DomainReference,
)

data class CreateTemplateRequest(
val file: FilePart,
)

interface CreateTemplateUseCase {
fun createTemplate(createTemplateRequest: CreateTemplateRequest): Mono<CreateTemplateResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.aamdigital.aambackendservice.export.core

data class ExportTemplate(
val id: String,
val templateId: String,
val title: String,
val description: String,
val applicableForEntityTypes: List<String>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.aamdigital.aambackendservice.export.core

import com.aamdigital.aambackendservice.domain.DomainReference
import com.fasterxml.jackson.databind.JsonNode
import org.springframework.core.io.buffer.DataBuffer
import reactor.core.publisher.Mono

interface RenderTemplateUseCase {
fun renderTemplate(templateRef: DomainReference, bodyData: JsonNode): Mono<DataBuffer>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.aamdigital.aambackendservice.export.core

import com.aamdigital.aambackendservice.domain.DomainReference
import reactor.core.publisher.Mono

interface TemplateStorage {
fun fetchTemplate(template: DomainReference): Mono<ExportTemplate>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.aamdigital.aambackendservice.export.di

import com.aamdigital.aambackendservice.auth.core.AuthConfig
import com.aamdigital.aambackendservice.auth.core.AuthProvider
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpHeaders
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.ClientRequest
import org.springframework.web.reactive.function.client.WebClient
import reactor.netty.http.client.HttpClient


@ConfigurationProperties("aam-render-api-client-configuration")
class AamRenderApiClientConfiguration(
val basePath: String,
val authConfig: AuthConfig,
val maxInMemorySizeInMegaBytes: Int = 16,
)

@Configuration
class AamRenderApiConfiguration {
companion object {
const val MEGA_BYTES_MULTIPLIER = 1024 * 1024
}

@Bean(name = ["aam-render-api-client"])
fun aamRenderApiClient(
@Qualifier("aam-keycloak") authProvider: AuthProvider,
configuration: AamRenderApiClientConfiguration
): WebClient {
val clientBuilder =
WebClient.builder()
.codecs {
it.defaultCodecs()
.maxInMemorySize(configuration.maxInMemorySizeInMegaBytes * MEGA_BYTES_MULTIPLIER)
}
.baseUrl(configuration.basePath)
.filter { request, next ->
authProvider.fetchToken(configuration.authConfig).map {
ClientRequest.from(request).header(HttpHeaders.AUTHORIZATION, "Bearer ${it.token}").build()
}.flatMap {
next.exchange(it)
}
}

return clientBuilder.clientConnector(ReactorClientHttpConnector(HttpClient.create())).build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.aamdigital.aambackendservice.export.di

import com.aamdigital.aambackendservice.couchdb.core.CouchDbClient
import com.aamdigital.aambackendservice.export.core.CreateTemplateUseCase
import com.aamdigital.aambackendservice.export.core.RenderTemplateUseCase
import com.aamdigital.aambackendservice.export.core.TemplateStorage
import com.aamdigital.aambackendservice.export.storage.DefaultTemplateStorage
import com.aamdigital.aambackendservice.export.usecase.DefaultCreateTemplateUseCase
import com.aamdigital.aambackendservice.export.usecase.DefaultRenderTemplateUseCase
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient

@Configuration
class UseCaseConfiguration {
@Bean(name = ["default-template-storage"])
fun defaultTemplateStorage(
couchDbClient: CouchDbClient,
): TemplateStorage = DefaultTemplateStorage(couchDbClient)

@Bean(name = ["default-create-template-use-case"])
fun defaultCreateTemplateUseCase(
@Qualifier("aam-render-api-client") webClient: WebClient,
objectMapper: ObjectMapper
): CreateTemplateUseCase = DefaultCreateTemplateUseCase(webClient, objectMapper)

@Bean(name = ["default-render-template-use-case"])
fun defaultRenderTemplateUseCase(
@Qualifier("aam-render-api-client") webClient: WebClient,
objectMapper: ObjectMapper,
templateStorage: TemplateStorage
): RenderTemplateUseCase = DefaultRenderTemplateUseCase(webClient, objectMapper, templateStorage)
}
Loading

0 comments on commit 916752e

Please sign in to comment.