diff --git a/README.md b/README.md index 7f8b2cb..7f81ddd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ repositories { } dependencies { - api("org.radarbase:radar-jersey:0.3.1") + api("org.radarbase:radar-jersey:0.3.2") } ``` diff --git a/build.gradle b/build.gradle index a1b2f4a..60eb9c4 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { allprojects { group = 'org.radarbase' - version = '0.3.1' + version = '0.3.2' ext { jerseyVersion = "2.32" diff --git a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/HibernateRepository.kt b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/HibernateRepository.kt index c9bb1b8..d155d13 100644 --- a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/HibernateRepository.kt +++ b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/HibernateRepository.kt @@ -8,27 +8,34 @@ import javax.persistence.EntityManager import javax.persistence.EntityTransaction open class HibernateRepository( - @Suppress("MemberVisibilityCanBePrivate") - protected val entityManager: Provider + private val entityManagerProvider: Provider ) { + @Suppress("MemberVisibilityCanBePrivate") + protected val entityManager: EntityManager + get() = entityManagerProvider.get() + + @Suppress("unused") + fun transact(transactionOperation: EntityManager.() -> T) = entityManager.transact(transactionOperation) + @Suppress("unused") + fun createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T) = entityManager.createTransaction(transactionOperation) + /** * Run a transaction and commit it. If an exception occurs, the transaction is rolled back. */ - open fun transact(transactionOperation: EntityManager.() -> T) = createTransaction { + open fun EntityManager.transact(transactionOperation: EntityManager.() -> T) = createTransaction { it.use { transactionOperation() } } /** * Start a transaction without committing it. If an exception occurs, the transaction is rolled back. */ - open fun createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { - val entityManager = entityManager.get() - val currentTransaction = entityManager.transaction + open fun EntityManager.createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { + val currentTransaction = transaction ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") currentTransaction.begin() try { - return entityManager.transactionOperation(object : CloseableTransaction { + return transactionOperation(object : CloseableTransaction { override val transaction: EntityTransaction = currentTransaction override fun close() { diff --git a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/DatabaseHealthMetricsTest.kt b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/DatabaseHealthMetricsTest.kt index d533fea..c7dbaf7 100644 --- a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/DatabaseHealthMetricsTest.kt +++ b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/DatabaseHealthMetricsTest.kt @@ -3,7 +3,10 @@ package org.radarbase.jersey.hibernate import okhttp3.OkHttpClient import okhttp3.Request import org.hamcrest.MatcherAssert +import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.radarbase.jersey.GrizzlyServer @@ -38,8 +41,8 @@ internal class DatabaseHealthMetricsTest { client.newCall(Request.Builder() .url("http://localhost:9091/health") .build()).execute().use { response -> - MatcherAssert.assertThat(response.isSuccessful, Matchers.`is`(true)) - MatcherAssert.assertThat(response.body?.string(), Matchers.equalTo("{\"status\":\"UP\",\"db\":{\"status\":\"UP\"}}")) + assertThat(response.isSuccessful, `is`(true)) + assertThat(response.body?.string(), equalTo("{\"status\":\"UP\",\"db\":{\"status\":\"UP\"}}")) } } finally { server.shutdown() @@ -92,8 +95,8 @@ internal class DatabaseHealthMetricsTest { client.newCall(Request.Builder() .url("http://localhost:9091/health") .build()).execute().use { response -> - MatcherAssert.assertThat(response.isSuccessful, Matchers.`is`(true)) - MatcherAssert.assertThat(response.body?.string(), Matchers.equalTo("{\"status\":\"UP\",\"db\":{\"status\":\"UP\"}}")) + assertThat(response.isSuccessful, `is`(true)) + assertThat(response.body?.string(), equalTo("{\"status\":\"UP\",\"db\":{\"status\":\"UP\"}}")) } // Disable database. Connections should now fail @@ -103,8 +106,8 @@ internal class DatabaseHealthMetricsTest { client.newCall(Request.Builder() .url("http://localhost:9091/health") .build()).execute().use { response -> - MatcherAssert.assertThat(response.isSuccessful, Matchers.`is`(true)) - MatcherAssert.assertThat(response.body?.string(), Matchers.equalTo("{\"status\":\"DOWN\",\"db\":{\"status\":\"DOWN\"}}")) + assertThat(response.isSuccessful, `is`(true)) + assertThat(response.body?.string(), equalTo("{\"status\":\"DOWN\",\"db\":{\"status\":\"DOWN\"}}")) } } finally { server.shutdown() diff --git a/src/main/kotlin/org/radarbase/jersey/auth/AuthConfig.kt b/src/main/kotlin/org/radarbase/jersey/auth/AuthConfig.kt index 724d8c0..9af0792 100644 --- a/src/main/kotlin/org/radarbase/jersey/auth/AuthConfig.kt +++ b/src/main/kotlin/org/radarbase/jersey/auth/AuthConfig.kt @@ -10,6 +10,8 @@ package org.radarbase.jersey.auth import com.fasterxml.jackson.annotation.JsonIgnore +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.radarbase.jersey.config.ConfigLoader.copyEnv import org.radarbase.jersey.config.ConfigLoader.copyOnChange import java.time.Duration @@ -49,6 +51,12 @@ data class MPConfig( /** Interval after which the list of subjects in a project should be refreshed (minutes). */ val syncParticipantsIntervalMin: Long = 5, ) { + @JsonIgnore + val httpUrl: HttpUrl? = url?.toHttpUrlOrNull() + ?.let { url -> + url.newBuilder().addPathSegment("").build() + } + /** Interval after which the list of projects should be refreshed. */ @JsonIgnore val syncProjectsInterval: Duration = Duration.ofMinutes(syncProjectsIntervalMin) diff --git a/src/main/kotlin/org/radarbase/jersey/config/RadarJerseyResourceEnhancer.kt b/src/main/kotlin/org/radarbase/jersey/config/RadarJerseyResourceEnhancer.kt index a82af2b..68c0832 100644 --- a/src/main/kotlin/org/radarbase/jersey/config/RadarJerseyResourceEnhancer.kt +++ b/src/main/kotlin/org/radarbase/jersey/config/RadarJerseyResourceEnhancer.kt @@ -34,26 +34,35 @@ import javax.ws.rs.ext.ContextResolver class RadarJerseyResourceEnhancer( private val config: AuthConfig ): JerseyResourceEnhancer { + var mapper: ObjectMapper = ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(JavaTimeModule()) + .registerModule(KotlinModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + var client: OkHttpClient = OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + override val classes = arrayOf( AuthenticationFilter::class.java, AuthorizationFeature::class.java) override fun ResourceConfig.enhance() { - register(ContextResolver { OBJECT_MAPPER }) + register(ContextResolver { mapper }) } override fun AbstractBinder.enhance() { bind(config.withEnv()) .to(AuthConfig::class.java) - bind(OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build()) + bind(client) .to(OkHttpClient::class.java) - bind(OBJECT_MAPPER) + bind(mapper) .to(ObjectMapper::class.java) // Bind factories. @@ -63,13 +72,4 @@ class RadarJerseyResourceEnhancer( .to(Auth::class.java) .`in`(RequestScoped::class.java) } - - companion object { - private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .registerModule(JavaTimeModule()) - .registerModule(KotlinModule()) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - } } diff --git a/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPClient.kt b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPClient.kt index dd6087c..6bcab3e 100644 --- a/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPClient.kt +++ b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPClient.kt @@ -2,8 +2,8 @@ package org.radarbase.jersey.service.managementportal import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectReader import okhttp3.* import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.radarbase.jersey.auth.Auth @@ -25,12 +25,16 @@ class MPClient( ?: throw IllegalArgumentException("Cannot configure managementportal client without client ID") private val clientSecret: String = config.managementPortal.clientSecret ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") - private val baseUrl: HttpUrl = config.managementPortal.url?.toHttpUrlOrNull() - ?: throw MalformedURLException("Cannot parse base URL ${config.managementPortal.url} as an URL") + private val baseUrl: HttpUrl = (config.managementPortal.url?.toHttpUrlOrNull() + ?: throw MalformedURLException("Cannot parse base URL ${config.managementPortal.url} as an URL")) + .newBuilder() + .addPathSegment("") + .build() - private val projectListReader = objectMapper.readerFor(object : TypeReference>() {}) - private val userListReader = objectMapper.readerFor(object : TypeReference>() {}) - private val tokenReader = objectMapper.readerFor(RestOauth2AccessToken::class.java) + private val projectListReader: ObjectReader by lazy { objectMapper.readerForListOf(MPProject::class.java) } + private val userListReader: ObjectReader by lazy { objectMapper.readerForListOf(MPUser::class.java) } + private val clientListReader: ObjectReader by lazy { objectMapper.readerForListOf(MPOAuthClient::class.java) } + private val tokenReader: ObjectReader by lazy { objectMapper.readerFor(RestOauth2AccessToken::class.java) } @Volatile private var token: RestOauth2AccessToken? = null @@ -83,6 +87,16 @@ class MPClient( .map { it.copy(projectId = projectId) } } + @Suppress("unused") + fun readClients(): List { + val request = Request.Builder().apply { + url(baseUrl.resolve("api/oauth-clients")!!) + header("Authorization", "Bearer ${ensureToken()}") + }.build() + + return httpClient.requestJson(request, clientListReader) + } + @JsonIgnoreProperties(ignoreUnknown = true) data class RestOauth2AccessToken( @JsonProperty("access_token") val accessToken: String, diff --git a/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPOAuthClient.kt b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPOAuthClient.kt new file mode 100644 index 0000000..316ea51 --- /dev/null +++ b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPOAuthClient.kt @@ -0,0 +1,7 @@ +package org.radarbase.jersey.service.managementportal + +import com.fasterxml.jackson.annotation.JsonProperty + +data class MPOAuthClient( + @JsonProperty("clientId") val id: String, +) diff --git a/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPProject.kt b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPProject.kt index c2c7109..1239389 100644 --- a/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPProject.kt +++ b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPProject.kt @@ -15,4 +15,5 @@ data class MPProject( /** Project description. */ val description: String? = null, /** Any other attributes. */ - val attributes: Map = emptyMap()) + val attributes: Map = emptyMap(), +) diff --git a/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPUser.kt b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPUser.kt index fbe870d..cfa4210 100644 --- a/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPUser.kt +++ b/src/main/kotlin/org/radarbase/jersey/service/managementportal/MPUser.kt @@ -13,4 +13,5 @@ data class MPUser( /** User status in the project. */ val status: String = "DEACTIVATED", /** Additional attributes of the user. */ - val attributes: Map = emptyMap()) + val attributes: Map = emptyMap(), +)