diff --git a/.github/workflows/snyk.yaml b/.github/workflows/snyk.yaml index a8e3119..d37e91e 100644 --- a/.github/workflows/snyk.yaml +++ b/.github/workflows/snyk.yaml @@ -29,4 +29,5 @@ jobs: --all-sub-projects --configuration-matching='^runtimeClasspath$' --org=radar-base - --policy-path=$PWD/.snyk + --policy-path=.snyk + --severity-threshold=high diff --git a/.snyk b/.snyk index 79c4305..9735564 100644 --- a/.snyk +++ b/.snyk @@ -8,3 +8,4 @@ ignore: expires: 2023-14-30T10:58:31.820Z created: 2022-10-31T10:58:31.828Z patch: {} +severityThreshold: high diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index da83967..04510a9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.9.10" + kotlin("jvm") version "1.9.22" } repositories { @@ -11,11 +11,11 @@ repositories { tasks.withType { compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) + jvmTarget.set(JvmTarget.JVM_17) } } -tasks.withType { - sourceCompatibility = "11" - targetCompatibility = "11" +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 3faa5ee..5020b0e 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,38 +1,38 @@ @Suppress("ConstPropertyName") object Versions { - const val project = "0.11.0" - const val kotlin = "1.9.10" + const val project = "0.11.1" + const val kotlin = "1.9.22" const val java: Int = 17 - const val jersey = "3.1.3" - const val grizzly = "4.0.0" - const val okhttp = "4.11.0" - const val junit = "5.10.0" + const val jersey = "3.1.5" + const val grizzly = "4.0.2" + const val okhttp = "4.12.0" + const val junit = "5.10.1" const val hamcrest = "2.2" - const val mockitoKotlin = "5.1.0" + const val mockitoKotlin = "5.2.1" - const val hk2 = "3.0.4" + const val hk2 = "3.0.5" const val managementPortal = "2.1.0" - const val radarCommons = "1.1.1" + const val radarCommons = "1.1.2" const val javaJwt = "4.4.0" const val jakartaWsRs = "3.1.0" const val jakartaAnnotation = "2.1.1" - const val jackson = "2.15.3" + const val jackson = "2.16.1" const val slf4j = "2.0.9" const val log4j2 = "2.20.0" const val jakartaXmlBind = "4.0.1" - const val jakartaJaxbCore = "4.0.3" - const val jakartaJaxbRuntime = "4.0.3" + const val jakartaJaxbCore = "4.0.4" + const val jakartaJaxbRuntime = "4.0.4" const val jakartaValidation = "3.0.2" const val hibernateValidator = "8.0.1.Final" const val glassfishJakartaEl = "4.0.2" const val jakartaActivation = "2.1.2" - const val swagger = "2.2.17" + const val swagger = "2.2.20" const val mustache = "0.9.11" - const val hibernate = "6.3.1.Final" - const val liquibase = "4.24.0" - const val postgres = "42.6.0" + const val hibernate = "6.4.2.Final" + const val liquibase = "4.25.1" + const val postgres = "42.7.2" const val h2 = "2.2.224" const val wrapper = "8.4" diff --git a/radar-jersey-hibernate/build.gradle.kts b/radar-jersey-hibernate/build.gradle.kts index befaf10..31beeab 100644 --- a/radar-jersey-hibernate/build.gradle.kts +++ b/radar-jersey-hibernate/build.gradle.kts @@ -31,3 +31,8 @@ dependencies { testImplementation("org.hamcrest:hamcrest:${Versions.hamcrest}") testImplementation("com.squareup.okhttp3:okhttp:${Versions.okhttp}") } + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} diff --git a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/DatabaseInitialization.kt b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/DatabaseInitialization.kt index f9ddf7a..1946886 100644 --- a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/DatabaseInitialization.kt +++ b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/DatabaseInitialization.kt @@ -4,19 +4,21 @@ import jakarta.persistence.EntityManager import jakarta.persistence.EntityManagerFactory import jakarta.ws.rs.core.Context import jakarta.ws.rs.ext.Provider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import liquibase.command.CommandScope import liquibase.command.core.UpdateCommandStep import liquibase.command.core.helpers.DbUrlConnectionCommandStep.DATABASE_ARG import liquibase.database.DatabaseFactory import liquibase.database.jvm.JdbcConnection import org.glassfish.jersey.server.monitoring.ApplicationEvent -import org.glassfish.jersey.server.monitoring.ApplicationEventListener import org.glassfish.jersey.server.monitoring.RequestEvent import org.glassfish.jersey.server.monitoring.RequestEventListener import org.hibernate.HibernateException import org.hibernate.Session -import org.radarbase.jersey.hibernate.RadarEntityManagerFactoryFactory.Companion.useEntityManager +import org.radarbase.jersey.coroutines.AsyncApplicationEventListener import org.radarbase.jersey.hibernate.config.DatabaseConfig +import org.radarbase.jersey.service.AsyncCoroutineService import org.slf4j.LoggerFactory import java.sql.Connection @@ -24,14 +26,17 @@ import java.sql.Connection class DatabaseInitialization( @Context private val entityManagerFactory: jakarta.inject.Provider, @Context private val dbConfig: DatabaseConfig, -) : ApplicationEventListener { + @Context private val asyncCoroutineService: AsyncCoroutineService, +) : AsyncApplicationEventListener(asyncCoroutineService) { - override fun onEvent(event: ApplicationEvent) { + override suspend fun process(event: ApplicationEvent) { if (event.type != ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) return + if (!dbConfig.liquibase.enable) return + try { - entityManagerFactory.get().useEntityManager { em -> - em.useConnection { connection -> - if (dbConfig.liquibase.enable) { + withContext(Dispatchers.IO) { + entityManagerFactory.get().useEntityManager { em -> + em.useConnection { connection -> initializeLiquibase(connection) } } diff --git a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactory.kt b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactory.kt index a04e7f4..f62ef77 100644 --- a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactory.kt +++ b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactory.kt @@ -3,6 +3,8 @@ package org.radarbase.jersey.hibernate import jakarta.persistence.EntityManager import jakarta.persistence.EntityManagerFactory import jakarta.ws.rs.core.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.glassfish.jersey.internal.inject.DisposableSupplier import org.slf4j.LoggerFactory @@ -26,3 +28,14 @@ class RadarEntityManagerFactory( private val logger = LoggerFactory.getLogger(RadarEntityManagerFactory::class.java) } } + +/** + * Run code with entity manager. Can be used for opening a repository outside of a request. + */ +@Suppress("unused") +suspend fun RadarEntityManagerFactory.useEntityManager(block: suspend (EntityManager) -> T): T = + withContext(Dispatchers.IO) { + get() + }.use { entityManager -> + block(entityManager) + } diff --git a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactoryFactory.kt b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactoryFactory.kt index 0e27a66..edf7318 100644 --- a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactoryFactory.kt +++ b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/RadarEntityManagerFactoryFactory.kt @@ -33,19 +33,16 @@ class RadarEntityManagerFactoryFactory( companion object { private val logger = LoggerFactory.getLogger(RadarEntityManagerFactoryFactory::class.java) + } +} - /** - * Use an EntityManager for the duration of [method]. No reference of the passed - * [EntityManager] should be returned, either directly or indirectly. - */ - @Suppress("unused") - inline fun EntityManagerFactory.useEntityManager(method: (EntityManager) -> T): T { - val entityManager = createEntityManager() - return try { - method(entityManager) - } finally { - entityManager.close() - } - } +/** + * Use an EntityManager for the duration of [method]. No reference of the passed + * [EntityManager] should be returned, either directly or indirectly. + */ +@Suppress("unused") +inline fun EntityManagerFactory.useEntityManager(method: (EntityManager) -> T): T { + return createEntityManager().use { entityManager -> + method(entityManager) } } diff --git a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancerFactory.kt b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancerFactory.kt index 58767ea..0344c97 100644 --- a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancerFactory.kt +++ b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancerFactory.kt @@ -7,7 +7,10 @@ import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.hibernate.config.DatabaseConfig import org.radarbase.jersey.hibernate.config.HibernateResourceEnhancer -class MockResourceEnhancerFactory(private val config: AuthConfig, private val databaseConfig: DatabaseConfig) : +class MockResourceEnhancerFactory( + private val config: AuthConfig, + private val databaseConfig: DatabaseConfig, +) : EnhancerFactory { override fun createEnhancers(): List = listOf( MockResourceEnhancer(), diff --git a/radar-jersey/build.gradle.kts b/radar-jersey/build.gradle.kts index 85cbbc2..5faf276 100644 --- a/radar-jersey/build.gradle.kts +++ b/radar-jersey/build.gradle.kts @@ -55,6 +55,11 @@ dependencies { testImplementation("org.mockito.kotlin:mockito-kotlin:${Versions.mockitoKotlin}") } +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + tasks.processResources { val properties = mapOf("version" to project.version) inputs.properties(properties) diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/coroutines/AsyncApplicationEventListener.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/coroutines/AsyncApplicationEventListener.kt new file mode 100644 index 0000000..f0b3a66 --- /dev/null +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/coroutines/AsyncApplicationEventListener.kt @@ -0,0 +1,27 @@ +package org.radarbase.jersey.coroutines + +import org.glassfish.jersey.server.monitoring.ApplicationEvent +import org.glassfish.jersey.server.monitoring.ApplicationEventListener +import org.glassfish.jersey.server.monitoring.RequestEvent +import org.glassfish.jersey.server.monitoring.RequestEventListener +import org.radarbase.jersey.service.AsyncCoroutineService + +/** Listen for application events. */ +abstract class AsyncApplicationEventListener( + private val asyncService: AsyncCoroutineService, +) : ApplicationEventListener { + override fun onEvent(event: ApplicationEvent?) { + event ?: return + asyncService.runBlocking { + process(event) + } + } + + /** + * Process incoming events. Inside processEvent a request scope is already present + * so repositories can be accessed. + */ + protected abstract suspend fun process(event: ApplicationEvent) + + override fun onRequest(requestEvent: RequestEvent?): RequestEventListener? = null +} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/coroutines/CoroutineRequestWrapper.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/coroutines/CoroutineRequestWrapper.kt index 5e4591e..4e168aa 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/coroutines/CoroutineRequestWrapper.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/coroutines/CoroutineRequestWrapper.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.glassfish.jersey.process.internal.RequestContext import org.glassfish.jersey.process.internal.RequestScope import org.radarbase.jersey.exception.HttpServerUnavailableException import org.slf4j.LoggerFactory @@ -14,28 +15,16 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class CoroutineRequestWrapper( - private val timeout: Duration? = 30.seconds, - requestScope: RequestScope? = null, - location: String? = null, + private val requestContext: RequestContext?, + config: CoroutineRequestConfig, ) { private val job = Job() + private val timeout = config.timeout val coroutineContext: CoroutineContext - private val requestContext = try { - if (requestScope != null) { - requestScope.suspendCurrent() - ?: requestScope.createContext() - } else { - null - } - } catch (ex: Throwable) { - logger.debug("Cannot create request scope: {}", ex.toString()) - null - } - init { - var context = job + contextName(location) + Dispatchers.Default + var context = job + contextName(config.location) + Dispatchers.Default if (requestContext != null) { context += CoroutineRequestContext(requestContext) } @@ -60,7 +49,6 @@ class CoroutineRequestWrapper( } companion object { - private val logger = LoggerFactory.getLogger(CoroutineRequestWrapper::class.java) @Suppress("DEPRECATION", "KotlinRedundantDiagnosticSuppress") private fun contextName(location: String?) = CoroutineName( @@ -68,3 +56,36 @@ class CoroutineRequestWrapper( ) } } + +data class CoroutineRequestConfig( + var timeout: Duration? = 30.seconds, + var requestScope: RequestScope? = null, + var location: String? = null, +) + +fun CoroutineRequestWrapper(requestScope: RequestScope? = null, block: CoroutineRequestConfig.(hasExistingScope: Boolean) -> Unit): CoroutineRequestWrapper { + var newlyCreated = false + val requestContext = try { + if (requestScope != null) { + requestScope.suspendCurrent() ?: run { + newlyCreated = true + requestScope.createContext() + } + } else { + null + } + } catch (ex: Throwable) { + logger.debug("Cannot create request scope: {}", ex.toString()) + null + } + val config = CoroutineRequestConfig().apply { + if (requestScope != null && requestContext != null) { + requestScope.runInScope(requestContext) { block(!newlyCreated) } + } else { + block(!newlyCreated) + } + } + return CoroutineRequestWrapper(requestContext, config) +} + +private val logger = LoggerFactory.getLogger(CoroutineRequestWrapper::class.java) diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/AsyncCoroutineService.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/AsyncCoroutineService.kt index 0040efb..9b17fbd 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/AsyncCoroutineService.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/AsyncCoroutineService.kt @@ -6,6 +6,11 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds interface AsyncCoroutineService { + /** + * Run a block with the async coroutine service contexts. If no request scope was created yet, + * this will create one within the coroutine. It can be accessed via [runInRequestScope]. + */ + suspend fun withContext(name: String = "AsyncCoroutineService.withContext", block: suspend () -> T): T /** * Run an AsyncResponse as a coroutine. The result of [block] will be used as the response. If diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/ScopedAsyncCoroutineService.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/ScopedAsyncCoroutineService.kt index c197160..96ae905 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/ScopedAsyncCoroutineService.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/service/ScopedAsyncCoroutineService.kt @@ -13,10 +13,12 @@ import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import org.glassfish.jersey.process.internal.RequestScope import org.radarbase.jersey.coroutines.CoroutineRequestContext import org.radarbase.jersey.coroutines.CoroutineRequestWrapper import org.radarbase.kotlin.coroutines.consumeFirst +import org.slf4j.LoggerFactory import kotlin.time.Duration class ScopedAsyncCoroutineService( @@ -51,6 +53,31 @@ class ScopedAsyncCoroutineService( } } + override suspend fun withContext(name: String, block: suspend () -> T): T { + // continue in existing context + return if (currentCoroutineContext()[CoroutineRequestContext.Key] != null) { + block() + } else { + val scope = try { + requestScope.get() + } catch (ex: Exception) { + logger.debug("Cannot construct request scope inside withContext", ex) + null + } + val wrapper = CoroutineRequestWrapper(scope) { + timeout = null + location = name + } + try { + withContext(wrapper.coroutineContext) { + block() + } + } finally { + wrapper.cancelRequest() + } + } + } + override fun runBlocking( timeout: Duration, block: suspend () -> T, @@ -74,12 +101,16 @@ class ScopedAsyncCoroutineService( } } - private inline fun withWrapper(timeout: Duration, block: CoroutineRequestWrapper.() -> V): V { - val wrapper = CoroutineRequestWrapper( - timeout, - requestScope.get(), - "${requestContext.get().method} ${uriInfo.get().path}", - ) + private inline fun withWrapper(timeout: Duration? = null, block: CoroutineRequestWrapper.() -> V): V { + val scope = requestScope.get() + val wrapper = CoroutineRequestWrapper(scope) { hasExistingScope -> + this.timeout = timeout + location = if (hasExistingScope) { + "${requestContext.get().method} ${uriInfo.get().path}" + } else { + "asyncCoroutine" + } + } return wrapper.block() } @@ -103,4 +134,8 @@ class ScopedAsyncCoroutineService( } } } + + companion object { + private val logger = LoggerFactory.getLogger(ScopedAsyncCoroutineService::class.java) + } }