diff --git a/README.md b/README.md index 7b9a316..8c17291 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ repositories { } dependencies { - api("org.radarbase:radar-jersey:0.7.0") + api("org.radarbase:radar-jersey:0.8.0") } ``` @@ -21,14 +21,14 @@ Any path or resource that should be authenticated against the ManagementPortal, @Path("/projects") @Authenticated class Users( - @Context projectService: MyProjectService + @Context projectService: MyProjectService ) { @GET @NeedsPermission(PROJECT, READ) fun getProjects(@Context auth: Auth): List { return projectService.read() .filter { auth.token.hasPermissionOnProject(PROJECT_READ, it.name) } - } + } @POST @Path("/{projectId}") @@ -53,40 +53,38 @@ class MyEnhancerFactory(private val config: MyConfigClass): EnhancerFactory { val authConfig = AuthConfig( managementPortal = MPConfig( url = "http://...", - ), + ), jwtResourceName = "res_MyResource", - ) + ) return listOf( // My own resource configuration MyResourceEnhancer(), // RADAR OAuth2 enhancement - ConfigLoader.Enhancers.radar(), + Enhancers.radar(authConfig), // Use ManagementPortal OAuth implementation - ConfigLoader.Enhancers.managementPortal, - // HttpApplicationException handling - ConfigLoader.Enhancers.httpException, - // General error handling (WebApplicationException and any other Exception) - ConfigLoader.Enhancers.generalException, + Enhancers.managementPortal(authConfig), + // Error handling + Enhancers.exception, ) } class MyResourceEnhancer: JerseyResourceEnhancer { override val classes: Array> = arrayOf( - ConfigLoader.Filters.logResponse, - ConfigLoader.Filters.cors, - ConfigLoader.Filters.cache, - ) + Filters.logResponse, + Filters.cors, + Filters.cache, + ) - overide val packages = arrayOf( - "com.example.app.resources", - ) + override val packages = arrayOf( + "com.example.app.resources", + ) override fun AbstractBinder.enhance() { bind(config) .to(MyConfigClass::class.java) - bind(MyService::class.java) - .to(MyService::class.java) - .`in`(Singleton::class.java) + bind(MyService::class.java) + .to(MyService::class.java) + .`in`(Singleton::class.java) } } } @@ -168,8 +166,8 @@ The implementation may optionally return health status `UP` or `DOWN` and may in ### Caching -Client side caching is enabled by the `ConfigLoader.Filters.cache` filter. When this is enabled, resource methods and classes can be annotated with a `org.radarbase.jersey.cache.Cache` or `NoCache` annotation. The fields of this annotation correspond to the [`Cache-Control` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). +Client side caching is enabled by the `Filters.cache` filter. When this is enabled, resource methods and classes can be annotated with a `org.radarbase.jersey.cache.Cache` or `NoCache` annotation. The fields of this annotation correspond to the [`Cache-Control` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). ### OpenAPI / Swagger -To automatically create a OpenAPI / Swagger endpoint for your API, add the `ConfigLoader.Enhancers.openapi` resource enhancer. Provide it with a general description of your API as specified by an `OpenAPI` object. +To automatically create a OpenAPI / Swagger endpoint for your API, add the `Enhancers.openapi` resource enhancer. Provide it with a general description of your API as specified by an `OpenAPI` object. diff --git a/build.gradle.kts b/build.gradle.kts index ef19bdd..de45fed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ plugins { allprojects { group = "org.radarbase" - version = "0.7.0" + version = "0.8.0" } subprojects { @@ -203,5 +203,5 @@ nexusPublishing { } tasks.wrapper { - gradleVersion = "7.2" + gradleVersion = "7.3.1" } diff --git a/gradle.properties b/gradle.properties index 79cfd06..94b5cb4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,14 +3,15 @@ org.gradle.jvmargs=-Xmx2000m org.gradle.vfs.watch=true kotlin.code.style=official -kotlinVersion=1.5.31 -dokkaVersion=1.5.31 +kotlinVersion=1.6.0 +dokkaVersion=1.6.0 jerseyVersion=3.0.3 grizzlyVersion=3.0.1 -okhttpVersion=4.9.2 -junitVersion=5.8.1 +okhttpVersion=4.9.3 +junitVersion=5.8.2 hamcrestVersion=2.2 +mockitoKotlinVersion=4.0.0 hk2Version=3.0.2 managementPortalVersion=0.8.0 @@ -29,6 +30,6 @@ swaggerVersion=2.1.11 mustacheVersion=0.9.10 hibernateVersion=5.6.1.Final -liquibaseVersion=4.5.0 +liquibaseVersion=4.6.1 postgresVersion=42.3.1 -h2Version=1.4.200 +h2Version=2.0.202 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..84d1f85 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/config/HibernateResourceEnhancer.kt b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/config/HibernateResourceEnhancer.kt index 89b09b3..c8f68e2 100644 --- a/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/config/HibernateResourceEnhancer.kt +++ b/radar-jersey-hibernate/src/main/kotlin/org/radarbase/jersey/hibernate/config/HibernateResourceEnhancer.kt @@ -2,7 +2,7 @@ package org.radarbase.jersey.hibernate.config import org.glassfish.jersey.internal.inject.AbstractBinder import org.glassfish.jersey.process.internal.RequestScoped -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.hibernate.DatabaseHealthMetrics import org.radarbase.jersey.hibernate.DatabaseInitialization import org.radarbase.jersey.hibernate.RadarEntityManagerFactory diff --git a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/HibernateTest.kt b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/HibernateTest.kt index eff2eb8..06fb525 100644 --- a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/HibernateTest.kt +++ b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/HibernateTest.kt @@ -77,20 +77,20 @@ internal class HibernateTest { override fun contentType() = "application/json".toMediaTypeOrNull() override fun writeTo(sink: BufferedSink) { - sink.writeUtf8("{\"name\": \"a\"}") + sink.writeUtf8("""{"name": "a"}""") } }) .url("http://localhost:9091/projects") .build()).execute().use { response -> assertThat(response.isSuccessful, `is`(true)) - assertThat(response.body?.string(), equalTo("{\"id\":1000,\"name\":\"a\",\"description\":null}")) + assertThat(response.body?.string(), equalTo("""{"id":1000,"name":"a"}""")) } client.newCall(Request.Builder() .url("http://localhost:9091/projects/1000") .build()).execute().use { response -> assertThat(response.isSuccessful, `is`(true)) - assertThat(response.body?.string(), equalTo("{\"id\":1000,\"name\":\"a\",\"description\":null}")) + assertThat(response.body?.string(), equalTo("""{"id":1000,"name":"a"}""")) } @@ -98,7 +98,7 @@ internal class HibernateTest { .url("http://localhost:9091/projects") .build()).execute().use { response -> assertThat(response.isSuccessful, `is`(true)) - assertThat(response.body?.string(), equalTo("[{\"id\":1000,\"name\":\"a\",\"description\":null}]")) + assertThat(response.body?.string(), equalTo("""[{"id":1000,"name":"a"}]""")) } client.newCall(Request.Builder() @@ -106,13 +106,13 @@ internal class HibernateTest { override fun contentType() = "application/json".toMediaTypeOrNull() override fun writeTo(sink: BufferedSink) { - sink.writeUtf8("{\"name\": \"a\",\"description\":\"d\"}") + sink.writeUtf8("""{"name": "a","description":"d"}""") } }) .url("http://localhost:9091/projects/1000") .build()).execute().use { response -> assertThat(response.isSuccessful, `is`(true)) - assertThat(response.body?.string(), equalTo("{\"id\":1000,\"name\":\"a\",\"description\":\"d\"}")) + assertThat(response.body?.string(), equalTo("""{"id":1000,"name":"a","description":"d"}""")) } client.newCall(Request.Builder() .delete() diff --git a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancer.kt b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancer.kt index 00ec951..157bc43 100644 --- a/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancer.kt +++ b/radar-jersey-hibernate/src/test/kotlin/org/radarbase/jersey/hibernate/mock/MockResourceEnhancer.kt @@ -1,16 +1,16 @@ package org.radarbase.jersey.hibernate.mock import org.glassfish.jersey.internal.inject.AbstractBinder -import org.radarbase.jersey.config.ConfigLoader -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.hibernate.db.ProjectRepository import org.radarbase.jersey.hibernate.db.ProjectRepositoryImpl import org.radarbase.jersey.service.ProjectService import jakarta.inject.Singleton +import org.radarbase.jersey.filter.Filters class MockResourceEnhancer : JerseyResourceEnhancer { override val classes: Array> = arrayOf( - ConfigLoader.Filters.logResponse) + Filters.logResponse) override val packages: Array = arrayOf( "org.radarbase.jersey.hibernate.mock.resource") 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 390bcc9..58767ea 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 @@ -1,19 +1,20 @@ package org.radarbase.jersey.hibernate.mock import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.config.ConfigLoader -import org.radarbase.jersey.config.EnhancerFactory -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.EnhancerFactory +import org.radarbase.jersey.enhancer.Enhancers +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) : EnhancerFactory { +class MockResourceEnhancerFactory(private val config: AuthConfig, private val databaseConfig: DatabaseConfig) : + EnhancerFactory { override fun createEnhancers(): List = listOf( - MockResourceEnhancer(), - ConfigLoader.Enhancers.radar(config), - HibernateResourceEnhancer(databaseConfig), - ConfigLoader.Enhancers.disabledAuthorization, - ConfigLoader.Enhancers.health, - ConfigLoader.Enhancers.httpException, - ConfigLoader.Enhancers.generalException) + MockResourceEnhancer(), + Enhancers.radar(config), + HibernateResourceEnhancer(databaseConfig), + Enhancers.disabledAuthorization, + Enhancers.health, + Enhancers.exception, + ) } diff --git a/radar-jersey/build.gradle.kts b/radar-jersey/build.gradle.kts index 4611c1b..7a5b32d 100644 --- a/radar-jersey/build.gradle.kts +++ b/radar-jersey/build.gradle.kts @@ -26,10 +26,13 @@ dependencies { val jerseyVersion: String by project api("org.glassfish.jersey.inject:jersey-hk2:$jerseyVersion") api("org.glassfish.jersey.core:jersey-server:$jerseyVersion") + implementation("org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion") val jacksonVersion: String by project api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion") val okhttpVersion: String by project implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") @@ -47,11 +50,6 @@ dependencies { val swaggerVersion: String by project implementation("io.swagger.core.v3:swagger-jaxrs2-jakarta:$swaggerVersion") - runtimeOnly("org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion") - - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") - runtimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") - runtimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion") val jakartaXmlBindVersion: String by project val jakartaJaxbCoreVersion: String by project @@ -70,6 +68,9 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") val hamcrestVersion: String by project testImplementation("org.hamcrest:hamcrest:$hamcrestVersion") + + val mockitoKotlinVersion: String by project + testImplementation("org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion") } tasks.processResources { diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/DisabledAuthorizationResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/disabled/DisabledAuthorizationResourceEnhancer.kt similarity index 88% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/DisabledAuthorizationResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/disabled/DisabledAuthorizationResourceEnhancer.kt index d687156..523cde0 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/DisabledAuthorizationResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/disabled/DisabledAuthorizationResourceEnhancer.kt @@ -7,12 +7,12 @@ * See the file LICENSE in the root of this repository. */ -package org.radarbase.jersey.config +package org.radarbase.jersey.auth.disabled import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.jersey.auth.AuthValidator -import org.radarbase.jersey.auth.disabled.DisabledAuthValidator import jakarta.inject.Singleton +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer /** * Registration for authorization against a ManagementPortal. It requires managementPortalUrl and diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/EcdsaResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/jwt/EcdsaResourceEnhancer.kt similarity index 90% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/EcdsaResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/jwt/EcdsaResourceEnhancer.kt index 4d6616b..a635066 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/EcdsaResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/jwt/EcdsaResourceEnhancer.kt @@ -7,12 +7,12 @@ * See the file LICENSE in the root of this repository. */ -package org.radarbase.jersey.config +package org.radarbase.jersey.auth.jwt import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.jersey.auth.AuthValidator -import org.radarbase.jersey.auth.jwt.EcdsaJwtTokenValidator import jakarta.inject.Singleton +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer /** * Registration for authorization against a generic OAuth 2.0 provider. diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/ManagementPortalResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/managementportal/ManagementPortalResourceEnhancer.kt similarity index 91% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/ManagementPortalResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/managementportal/ManagementPortalResourceEnhancer.kt index 570a3a6..775323b 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/ManagementPortalResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/auth/managementportal/ManagementPortalResourceEnhancer.kt @@ -7,14 +7,12 @@ * See the file LICENSE in the root of this repository. */ -package org.radarbase.jersey.config +package org.radarbase.jersey.auth.managementportal import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.auth.authentication.TokenValidator import org.radarbase.jersey.auth.AuthConfig import org.radarbase.jersey.auth.AuthValidator -import org.radarbase.jersey.auth.managementportal.ManagementPortalTokenValidator -import org.radarbase.jersey.auth.managementportal.TokenValidatorFactory import org.radarbase.jersey.service.ProjectService import org.radarbase.jersey.service.managementportal.MPClientFactory import org.radarbase.jersey.service.managementportal.MPProjectService @@ -22,6 +20,7 @@ import org.radarbase.jersey.service.managementportal.ProjectServiceWrapper import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.management.client.MPClient import jakarta.inject.Singleton +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer /** * Registration for authorization against a ManagementPortal. It requires managementPortalUrl and diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/ConfigLoader.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/ConfigLoader.kt index 9663990..357c1b6 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/ConfigLoader.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/ConfigLoader.kt @@ -3,14 +3,9 @@ package org.radarbase.jersey.config import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.KotlinModule -import io.swagger.v3.oas.models.OpenAPI import org.glassfish.jersey.internal.inject.AbstractBinder import org.glassfish.jersey.server.ResourceConfig -import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.cache.CacheControlFeature -import org.radarbase.jersey.doc.swagger.SwaggerResourceEnhancer -import org.radarbase.jersey.filter.CorsFilter -import org.radarbase.jersey.filter.ResponseLoggerFilter +import org.radarbase.jersey.enhancer.* import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.BufferedInputStream @@ -104,48 +99,6 @@ object ConfigLoader { val logger: Logger = LoggerFactory.getLogger(ConfigLoader::class.java) - object Filters { - /** Adds CORS headers to all responses. */ - val cors = CorsFilter::class.java - /** Log the HTTP status responses of all requests. */ - val logResponse = ResponseLoggerFilter::class.java - /** Add cache control headers to responses. */ - val cache = CacheControlFeature::class.java - } - object Enhancers { - /** Adds authorization framework, configuration and utilities. */ - fun radar( - config: AuthConfig, - includeMapper: Boolean = true, - includeHttpClient: Boolean = true, - ) = RadarJerseyResourceEnhancer(config, includeMapper = includeMapper, includeHttpClient = includeHttpClient) - /** Authorization via ManagementPortal. */ - fun managementPortal(config: AuthConfig) = ManagementPortalResourceEnhancer(config) - /** Disable all authorization. Useful for a public service. */ - val disabledAuthorization = DisabledAuthorizationResourceEnhancer() - /** Handle a generic ECDSA identity provider. */ - val ecdsa = EcdsaResourceEnhancer() - /** Adds a health endpoint. */ - val health = HealthResourceEnhancer() - /** - * Handles any HTTP application exceptions including an appropriate response to client. - * @see org.radarbase.jersey.exception.HttpApplicationException - */ - val httpException = HttpExceptionResourceEnhancer() - /** Handle unhandled exceptions. */ - val generalException = GeneralExceptionResourceEnhancer() - /** Adds OkHttpClient utility. Not needed if radar(includeHttpClient = true). */ - val okhttp = OkHttpResourceEnhancer() - /** Add ObjectMapper utility. Not needed if radar(includeMapper = true). */ - val mapper = MapperResourceEnhancer() - /** - * Adds an OpenAPI endpoint to the stack at `/openapi.yaml` and `/openapi.json`. - * The description is given with [openApi]. Any routes provided in - * [ignoredRoutes] will not be shown in the endpoint. - */ - fun swagger(openApi: OpenAPI, ignoredRoutes: Set? = null) = SwaggerResourceEnhancer(openApi, ignoredRoutes) - } - inline fun T.copyEnv(key: String, doCopy: T.(String?) -> T): T = copyOnChange(null, { System.getenv(key) }, doCopy) inline fun T.copyOnChange(original: V, modification: (V) -> V, doCopy: T.(V) -> T): T { diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/GeneralExceptionResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/GeneralExceptionResourceEnhancer.kt deleted file mode 100644 index 0375243..0000000 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/GeneralExceptionResourceEnhancer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.radarbase.jersey.config - -import org.radarbase.jersey.exception.ClientAbortExceptionWriterInterceptor -import org.radarbase.jersey.exception.mapper.UnhandledExceptionMapper -import org.radarbase.jersey.exception.mapper.WebApplicationExceptionMapper - -/** Add WebApplicationException and any exception handling. */ -class GeneralExceptionResourceEnhancer: JerseyResourceEnhancer { - override val classes: Array> = arrayOf( - ClientAbortExceptionWriterInterceptor::class.java, - UnhandledExceptionMapper::class.java, - WebApplicationExceptionMapper::class.java, - ) -} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/HttpExceptionResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/HttpExceptionResourceEnhancer.kt deleted file mode 100644 index 5215bf0..0000000 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/HttpExceptionResourceEnhancer.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.radarbase.jersey.config - -import org.glassfish.jersey.internal.inject.AbstractBinder -import org.glassfish.jersey.internal.inject.PerThread -import org.radarbase.jersey.exception.mapper.* -import jakarta.inject.Singleton - -/** Add HttpApplicationException handling. This includes a HTML templating solution. */ -class HttpExceptionResourceEnhancer: JerseyResourceEnhancer { - override val classes: Array> = arrayOf( - HttpApplicationExceptionMapper::class.java) - - override fun AbstractBinder.enhance() { - bind(HtmlTemplateExceptionRenderer::class.java) - .to(ExceptionRenderer::class.java) - .named("text/html") - .`in`(PerThread::class.java) - - bind(DefaultJsonExceptionRenderer::class.java) - .to(ExceptionRenderer::class.java) - .named("application/json") - .`in`(Singleton::class.java) - - bind(DefaultTextExceptionRenderer::class.java) - .to(ExceptionRenderer::class.java) - .named("text/plain") - .`in`(Singleton::class.java) - } -} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/doc/swagger/SwaggerResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/doc/swagger/SwaggerResourceEnhancer.kt index b720894..1bc1cf1 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/doc/swagger/SwaggerResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/doc/swagger/SwaggerResourceEnhancer.kt @@ -4,7 +4,7 @@ import io.swagger.v3.jaxrs2.integration.JaxrsOpenApiContextBuilder import io.swagger.v3.oas.integration.SwaggerConfiguration import io.swagger.v3.oas.models.OpenAPI import org.glassfish.jersey.server.ResourceConfig -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer /** * Adds an OpenAPI endpoint to the stack at `/openapi.yaml` and `/openapi.json`. diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/EnhancerFactory.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/EnhancerFactory.kt similarity index 77% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/EnhancerFactory.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/EnhancerFactory.kt index 65b4a67..a18a10a 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/EnhancerFactory.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/EnhancerFactory.kt @@ -1,8 +1,8 @@ -package org.radarbase.jersey.config +package org.radarbase.jersey.enhancer /** * Factory to create resource enhancers with. */ interface EnhancerFactory { fun createEnhancers(): List -} \ No newline at end of file +} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/Enhancers.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/Enhancers.kt new file mode 100644 index 0000000..a6ad3e3 --- /dev/null +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/Enhancers.kt @@ -0,0 +1,41 @@ +package org.radarbase.jersey.enhancer + +import io.swagger.v3.oas.models.OpenAPI +import org.radarbase.jersey.auth.AuthConfig +import org.radarbase.jersey.auth.disabled.DisabledAuthorizationResourceEnhancer +import org.radarbase.jersey.auth.jwt.EcdsaResourceEnhancer +import org.radarbase.jersey.auth.managementportal.ManagementPortalResourceEnhancer +import org.radarbase.jersey.doc.swagger.SwaggerResourceEnhancer +import org.radarbase.jersey.exception.ExceptionResourceEnhancer + +object Enhancers { + /** Adds authorization framework, configuration and utilities. */ + fun radar( + config: AuthConfig, + includeMapper: Boolean = true, + includeHttpClient: Boolean = true, + ) = RadarJerseyResourceEnhancer(config, includeMapper = includeMapper, includeHttpClient = includeHttpClient) + /** Authorization via ManagementPortal. */ + fun managementPortal(config: AuthConfig) = ManagementPortalResourceEnhancer(config) + /** Disable all authorization. Useful for a public service. */ + val disabledAuthorization = DisabledAuthorizationResourceEnhancer() + /** Handle a generic ECDSA identity provider. */ + val ecdsa = EcdsaResourceEnhancer() + /** Adds a health endpoint. */ + val health = HealthResourceEnhancer() + /** + * Handles any application exceptions including an appropriate response to client. + * @see org.radarbase.jersey.exception.HttpApplicationException + */ + val exception = ExceptionResourceEnhancer() + /** Adds OkHttpClient utility. Not needed if radar(includeHttpClient = true). */ + val okhttp = OkHttpResourceEnhancer() + /** Add ObjectMapper utility. Not needed if radar(includeMapper = true). */ + val mapper = MapperResourceEnhancer() + /** + * Adds an OpenAPI endpoint to the stack at `/openapi.yaml` and `/openapi.json`. + * The description is given with [openApi]. Any routes provided in + * [ignoredRoutes] will not be shown in the endpoint. + */ + fun swagger(openApi: OpenAPI, ignoredRoutes: Set? = null) = SwaggerResourceEnhancer(openApi, ignoredRoutes) +} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/HealthResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/HealthResourceEnhancer.kt similarity index 93% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/HealthResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/HealthResourceEnhancer.kt index 0bc5535..10aed2d 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/HealthResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/HealthResourceEnhancer.kt @@ -1,4 +1,4 @@ -package org.radarbase.jersey.config +package org.radarbase.jersey.enhancer import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.jersey.resource.HealthResource diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/JerseyResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/JerseyResourceEnhancer.kt similarity index 96% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/JerseyResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/JerseyResourceEnhancer.kt index 5d6ac18..697f5b8 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/JerseyResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/JerseyResourceEnhancer.kt @@ -7,7 +7,7 @@ * See the file LICENSE in the root of this repository. */ -package org.radarbase.jersey.config +package org.radarbase.jersey.enhancer import org.glassfish.jersey.internal.inject.AbstractBinder import org.glassfish.jersey.server.ResourceConfig diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/MapperResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/MapperResourceEnhancer.kt similarity index 76% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/MapperResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/MapperResourceEnhancer.kt index 1d604a1..76b925e 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/MapperResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/MapperResourceEnhancer.kt @@ -7,12 +7,13 @@ * See the file LICENSE in the root of this repository. */ -package org.radarbase.jersey.config +package org.radarbase.jersey.enhancer import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule @@ -20,6 +21,7 @@ import jakarta.inject.Singleton import jakarta.ws.rs.ext.ContextResolver import org.glassfish.jersey.internal.inject.AbstractBinder import org.glassfish.jersey.server.ResourceConfig +import org.slf4j.LoggerFactory /** * Add utilities such as a reusable ObjectMapper and OkHttpClient to inject. @@ -33,26 +35,32 @@ class MapperResourceEnhancer: JerseyResourceEnhancer { get() = mapper ?: createDefaultMapper().also { mapper = it } override fun ResourceConfig.enhance() { - register(ContextResolver { latestMapper }) + register(ObjectMapperResolver()) } override fun AbstractBinder.enhance() { bind(latestMapper) .to(ObjectMapper::class.java) - .`in`(Singleton::class.java) } companion object { fun createDefaultMapper(): ObjectMapper = jsonMapper { + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) serializationInclusion(JsonInclude.Include.NON_NULL) - addModule(JavaTimeModule()) addModule(kotlinModule { nullToEmptyMap(true) nullToEmptyCollection(true) nullIsSameAsDefault(true) }) - configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + addModule(JavaTimeModule()) + addModule(Jdk8Module()) + } + } + + private inner class ObjectMapperResolver : ContextResolver { + override fun getContext(type: Class<*>?): ObjectMapper { + return latestMapper } } } diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/OkHttpResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/OkHttpResourceEnhancer.kt similarity index 96% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/OkHttpResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/OkHttpResourceEnhancer.kt index 0e55e0a..59de668 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/OkHttpResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/OkHttpResourceEnhancer.kt @@ -7,7 +7,7 @@ * See the file LICENSE in the root of this repository. */ -package org.radarbase.jersey.config +package org.radarbase.jersey.enhancer import jakarta.inject.Singleton import okhttp3.OkHttpClient diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/RadarJerseyResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/RadarJerseyResourceEnhancer.kt similarity index 94% rename from radar-jersey/src/main/kotlin/org/radarbase/jersey/config/RadarJerseyResourceEnhancer.kt rename to radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/RadarJerseyResourceEnhancer.kt index 2453252..b624c32 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/config/RadarJerseyResourceEnhancer.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/enhancer/RadarJerseyResourceEnhancer.kt @@ -7,10 +7,11 @@ * See the file LICENSE in the root of this repository. */ -package org.radarbase.jersey.config +package org.radarbase.jersey.enhancer import jakarta.inject.Singleton import org.glassfish.jersey.internal.inject.AbstractBinder +import org.glassfish.jersey.jackson.JacksonFeature import org.glassfish.jersey.process.internal.RequestScoped import org.glassfish.jersey.server.ResourceConfig import org.radarbase.jersey.auth.Auth @@ -44,6 +45,7 @@ class RadarJerseyResourceEnhancer( ) override fun ResourceConfig.enhance() { + register(JacksonFeature.withoutExceptionMappers()) okHttpResourceEnhancer?.enhanceResources(this) mapperResourceEnhancer?.enhanceResources(this) } diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/ExceptionResourceEnhancer.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/ExceptionResourceEnhancer.kt new file mode 100644 index 0000000..8562193 --- /dev/null +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/ExceptionResourceEnhancer.kt @@ -0,0 +1,41 @@ +package org.radarbase.jersey.exception + +import jakarta.inject.Singleton +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.glassfish.jersey.internal.inject.PerThread +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer +import org.radarbase.jersey.exception.mapper.* + +/** Add WebApplicationException and any exception handling. */ +class ExceptionResourceEnhancer: JerseyResourceEnhancer { + /** + * Renderers to use, per mediatype. To use different renderers, override the renderer that + * should be overridden. + */ + val renderers: MutableMap> = mutableMapOf( + "text/html" to HtmlTemplateExceptionRenderer::class.java, + "application/json" to DefaultJsonExceptionRenderer::class.java, + "text/plain" to DefaultTextExceptionRenderer::class.java, + ) + + override var classes: Array> = arrayOf( + ClientAbortExceptionWriterInterceptor::class.java, + UnhandledExceptionMapper::class.java, + WebApplicationExceptionMapper::class.java, + HttpApplicationExceptionMapper::class.java, + JsonProcessingExceptionMapper::class.java, + ) + + override fun AbstractBinder.enhance() { + renderers.forEach { (name, rendererClass) -> + bind(rendererClass) + .to(ExceptionRenderer::class.java) + .named(name) + .`in`(PerThread::class.java) + } + + bind(ExceptionRenderers::class.java) + .to(ExceptionRenderers::class.java) + .`in`(Singleton::class.java) + } +} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/ExceptionRenderers.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/ExceptionRenderers.kt new file mode 100644 index 0000000..7455f0f --- /dev/null +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/ExceptionRenderers.kt @@ -0,0 +1,50 @@ +package org.radarbase.jersey.exception.mapper + +import jakarta.inject.Singleton +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.Provider +import org.glassfish.hk2.api.IterableProvider +import org.radarbase.jersey.exception.HttpApplicationException +import org.slf4j.LoggerFactory + +@Singleton +@Provider +class ExceptionRenderers( + @Context private val requestContext: ContainerRequestContext, + @Context private val renderers: IterableProvider, +) { + private val mediaType: MediaType + get() = requestContext.acceptableMediaTypes + .firstOrNull { type -> type in supportedTypes } + .takeIf { it != MediaType.WILDCARD_TYPE } + ?: MediaType.APPLICATION_JSON_TYPE + + fun render(ex: HttpApplicationException): Response.ResponseBuilder { + val mediaType = mediaType + val renderer = renderers.named(mediaType.toString()).firstOrNull() + if (renderer == null) { + logger.error("Cannot render exception with type {}: no renderer registered", mediaType) + return Response.status(ex.status) + } + + val entity = renderer.render(ex) + + val builder = Response.status(ex.status) + .entity(entity) + .header("Content-Type", mediaType.withCharset("utf-8").toString()) + + ex.additionalHeaders.forEach { (name, value) -> + builder.header(name, value) + } + + return builder + } + + companion object { + private val logger = LoggerFactory.getLogger(ExceptionRenderers::class.java) + private val supportedTypes = setOf(MediaType.WILDCARD_TYPE, MediaType.APPLICATION_JSON_TYPE, MediaType.TEXT_HTML_TYPE, MediaType.TEXT_PLAIN_TYPE) + } +} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/HttpApplicationExceptionMapper.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/HttpApplicationExceptionMapper.kt index e0b21d5..7152e0c 100644 --- a/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/HttpApplicationExceptionMapper.kt +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/HttpApplicationExceptionMapper.kt @@ -9,56 +9,37 @@ package org.radarbase.jersey.exception.mapper -import org.glassfish.hk2.api.IterableProvider -import org.radarbase.jersey.exception.HttpApplicationException -import org.slf4j.LoggerFactory import jakarta.inject.Singleton import jakarta.ws.rs.container.ContainerRequestContext import jakarta.ws.rs.core.Context -import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response import jakarta.ws.rs.core.UriInfo import jakarta.ws.rs.ext.ExceptionMapper import jakarta.ws.rs.ext.Provider +import org.radarbase.jersey.exception.HttpApplicationException +import org.slf4j.LoggerFactory @Provider @Singleton class HttpApplicationExceptionMapper( @Context private val uriInfo: UriInfo, @Context private val requestContext: ContainerRequestContext, - @Context private val renderers: IterableProvider + @Context private val renderers: ExceptionRenderers, ) : ExceptionMapper { override fun toResponse(exception: HttpApplicationException): Response { - val mediaType = requestContext.acceptableMediaTypes - .firstOrNull { type -> type in supportedTypes } - .takeIf { it != MediaType.WILDCARD_TYPE } - ?: MediaType.APPLICATION_JSON_TYPE - - logger.error("[{}] {} {} - {}: {}", exception.status, requestContext.method, uriInfo.path, exception.code, exception.detailedMessage) - - val renderer = renderers.named(mediaType.toString()).firstOrNull() - - if (renderer == null) { - logger.error("Cannot render exception with type {}: no renderer registered", mediaType) - return Response.status(exception.status).build() - } - - val entity = renderer.render(exception) - - val responseBuilder = Response.status(exception.status) - .entity(entity) - .header("Content-Type", mediaType.withCharset("utf-8").toString()) - - exception.additionalHeaders.forEach { (name, value) -> - responseBuilder.header(name, value) - } - - return responseBuilder.build() + logger.error( + "[{}] {} {} - {}: {}", + exception.status, + requestContext.method, + uriInfo.path, + exception.code, + exception.detailedMessage + ) + + return renderers.render(exception).build() } companion object { private val logger = LoggerFactory.getLogger(HttpApplicationExceptionMapper::class.java) - - private val supportedTypes = setOf(MediaType.WILDCARD_TYPE, MediaType.APPLICATION_JSON_TYPE, MediaType.TEXT_HTML_TYPE, MediaType.TEXT_PLAIN_TYPE) } } diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/JsonProcessingExceptionMapper.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/JsonProcessingExceptionMapper.kt new file mode 100644 index 0000000..3977223 --- /dev/null +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/exception/mapper/JsonProcessingExceptionMapper.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019. The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * See the file LICENSE in the root of this repository. + */ + +package org.radarbase.jersey.exception.mapper + +import com.fasterxml.jackson.core.JsonProcessingException +import org.slf4j.LoggerFactory +import jakarta.inject.Singleton +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.core.UriInfo +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider +import org.radarbase.jersey.exception.HttpBadRequestException + +/** Handle exceptions without a specific mapper. */ +@Provider +@Singleton +class JsonProcessingExceptionMapper( + @Context private val uriInfo: UriInfo, + @Context private val requestContext: ContainerRequestContext, + @Context private val renderers: ExceptionRenderers, +) : ExceptionMapper { + + override fun toResponse(exception: JsonProcessingException): Response { + logger.error("[400] {} {} {}", requestContext.method, uriInfo.path, exception.toString()) + return renderers + .render(HttpBadRequestException("json_processing", "Failed to map JSON: $exception")) + .build() + } + + companion object { + private val logger = LoggerFactory.getLogger(JsonProcessingExceptionMapper::class.java) + } +} diff --git a/radar-jersey/src/main/kotlin/org/radarbase/jersey/filter/Filters.kt b/radar-jersey/src/main/kotlin/org/radarbase/jersey/filter/Filters.kt new file mode 100644 index 0000000..e63076c --- /dev/null +++ b/radar-jersey/src/main/kotlin/org/radarbase/jersey/filter/Filters.kt @@ -0,0 +1,12 @@ +package org.radarbase.jersey.filter + +import org.radarbase.jersey.cache.CacheControlFeature + +object Filters { + /** Adds CORS headers to all responses. */ + val cors = CorsFilter::class.java + /** Log the HTTP status responses of all requests. */ + val logResponse = ResponseLoggerFilter::class.java + /** Add cache control headers to responses. */ + val cache = CacheControlFeature::class.java +} diff --git a/radar-jersey/src/test/kotlin/org/radarbase/jersey/auth/RadarJerseyResourceEnhancerTest.kt b/radar-jersey/src/test/kotlin/org/radarbase/jersey/auth/RadarJerseyResourceEnhancerTest.kt index 5e9c41b..aac9cfe 100644 --- a/radar-jersey/src/test/kotlin/org/radarbase/jersey/auth/RadarJerseyResourceEnhancerTest.kt +++ b/radar-jersey/src/test/kotlin/org/radarbase/jersey/auth/RadarJerseyResourceEnhancerTest.kt @@ -9,8 +9,11 @@ package org.radarbase.jersey.auth +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response import org.glassfish.grizzly.http.server.HttpServer import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory import org.hamcrest.MatcherAssert.assertThat @@ -57,22 +60,63 @@ internal class RadarJerseyResourceEnhancerTest { @Test fun testBasicGet() { + client.request( + "http://localhost:9091/", + callback = { response -> + assertThat(response.isSuccessful, `is`(true)) + assertThat(response.body?.string(), equalTo("{\"this\":\"that\"}")) + }, + ) + } + + @Test + fun testAuthenticatedGet() { + client.request( + "http://localhost:9091/user", + buildRequest = { + bearerHeader(oauthHelper) + }, + callback = { response -> + assertThat(response.isSuccessful, `is`(true)) + assertThat(response.body?.string(), equalTo("""{"accessToken":"${oauthHelper.validEcToken}"}""")) + } + ) + } + + @Test + fun testAuthenticatedGetDetailed() { client.newCall(Request.Builder() - .url("http://localhost:9091/") - .build()).execute().use { response -> + .url("http://localhost:9091/user/detailed") + .bearerHeader(oauthHelper) + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(true)) - assertThat(response.body?.string(), equalTo("{\"this\":\"that\"}")) + assertThat(response.body?.string(), equalTo("""{"accessToken":"${oauthHelper.validEcToken}","name":"name","createdAt":"1970-01-01T01:00:00Z"}""")) } } @Test - fun testAuthenticatedGet() { + fun testAuthenticatedPostDetailed() { client.newCall(Request.Builder() - .url("http://localhost:9091/user") - .bearerHeader(oauthHelper) - .build()).execute().use { response -> + .url("http://localhost:9091/user") + .bearerHeader(oauthHelper) + .post("""{"accessToken":"${oauthHelper.validEcToken}","name":"name","createdAt":"1970-01-01T01:00:00Z"}""".toRequestBody("application/json".toMediaType())) + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(true)) - assertThat(response.body?.string(), equalTo("{\"accessToken\":\"${oauthHelper.validEcToken}\"}")) + assertThat(response.body?.string(), equalTo("""{"accessToken":"${oauthHelper.validEcToken}","name":"name","createdAt":"1970-01-01T01:00:00Z"}""")) + } + } + + @Test + fun testAuthenticatedPostDetailedBadRequest() { + client.newCall(Request.Builder() + .url("http://localhost:9091/user") + .bearerHeader(oauthHelper) + .post("""{}""".toRequestBody("application/json".toMediaType())) + .build() + ).execute().use { response -> + assertThat(response.code, `is`(400)) } } @@ -80,9 +124,10 @@ internal class RadarJerseyResourceEnhancerTest { @Test fun testUnauthenticatedGet() { client.newCall(Request.Builder() - .url("http://localhost:9091/user") - .header("Accept", "application/json") - .build()).execute().use { response -> + .url("http://localhost:9091/user") + .header("Accept", "application/json") + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(false)) assertThat(response.code, `is`(401)) assertThat(response.body?.string(), equalTo("{\"error\":\"token_missing\",\"error_description\":\"No bearer token is provided in the request.\"}")) @@ -92,9 +137,10 @@ internal class RadarJerseyResourceEnhancerTest { @Test fun testUnauthenticatedGetNoAcceptHeader() { client.newCall(Request.Builder() - .url("http://localhost:9091/user") - .header("Accept", "*/*") - .build()).execute().use { response -> + .url("http://localhost:9091/user") + .header("Accept", "*/*") + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(false)) assertThat(response.code, `is`(401)) assertThat(response.body?.string(), equalTo("{\"error\":\"token_missing\",\"error_description\":\"No bearer token is provided in the request.\"}")) @@ -104,10 +150,11 @@ internal class RadarJerseyResourceEnhancerTest { @Test fun testBadAuthenticationGet() { client.newCall(Request.Builder() - .url("http://localhost:9091/user") - .header("Accept", "application/json") - .header("Authorization", "Bearer abcdef") - .build()).execute().use { response -> + .url("http://localhost:9091/user") + .header("Accept", "application/json") + .header("Authorization", "Bearer abcdef") + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(false)) assertThat(response.code, `is`(401)) assertThat(response.body?.string(), equalTo("{\"error\":\"token_unverified\",\"error_description\":\"Cannot verify token. It may have been rendered invalid.\"}")) @@ -117,23 +164,24 @@ internal class RadarJerseyResourceEnhancerTest { @Test fun testExistingGet() { client.newCall(Request.Builder() - .url("http://localhost:9091/projects/a/users/b") - .bearerHeader(oauthHelper) - .build()).execute().use { response -> + .url("http://localhost:9091/projects/a/users/b") + .bearerHeader(oauthHelper) + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(true)) assertThat(response.body?.string(), equalTo("{\"projectId\":\"a\",\"userId\":\"b\"}")) } } - @Test fun testNonExistingGet() { client.newCall(Request.Builder() - .url("http://localhost:9091/projects/c/users/b") - .bearerHeader(oauthHelper) - .header("Accept", "application/json") - .build()).execute().use { response -> + .url("http://localhost:9091/projects/c/users/b") + .bearerHeader(oauthHelper) + .header("Accept", "application/json") + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(false)) assertThat(response.code, `is`(404)) @@ -144,10 +192,11 @@ internal class RadarJerseyResourceEnhancerTest { @Test fun testNonExistingGetHtml() { client.newCall(Request.Builder() - .url("http://localhost:9091/projects/c/users/b") - .bearerHeader(oauthHelper) - .header("Accept", "text/html,application/json") - .build()).execute().use { response -> + .url("http://localhost:9091/projects/c/users/b") + .bearerHeader(oauthHelper) + .header("Accept", "text/html,application/json") + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(false)) assertThat(response.code, `is`(404)) @@ -162,10 +211,11 @@ internal class RadarJerseyResourceEnhancerTest { @Test fun testNonExistingGetBrowser() { client.newCall(Request.Builder() - .url("http://localhost:9091/projects/c/users/b") - .bearerHeader(oauthHelper) - .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - .build()).execute().use { response -> + .url("http://localhost:9091/projects/c/users/b") + .bearerHeader(oauthHelper) + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .build() + ).execute().use { response -> assertThat(response.isSuccessful, `is`(false)) assertThat(response.code, `is`(404)) @@ -217,4 +267,18 @@ internal class RadarJerseyResourceEnhancerTest { assertThat(body, equalTo("")) } } + + companion object { + private fun OkHttpClient.request( + url: String, + buildRequest: (Request.Builder.() -> Unit)? = null, + callback: (Response) -> Unit, + ) { + val requestBuilder = Request.Builder().url(url) + if (buildRequest != null) { + requestBuilder.buildRequest() + } + newCall(requestBuilder.build()).execute().use(callback) + } + } } diff --git a/radar-jersey/src/test/kotlin/org/radarbase/jersey/enhancer/MapperResourceEnhancerTest.kt b/radar-jersey/src/test/kotlin/org/radarbase/jersey/enhancer/MapperResourceEnhancerTest.kt new file mode 100644 index 0000000..ad30709 --- /dev/null +++ b/radar-jersey/src/test/kotlin/org/radarbase/jersey/enhancer/MapperResourceEnhancerTest.kt @@ -0,0 +1,36 @@ +package org.radarbase.jersey.enhancer + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.ws.rs.ext.ContextResolver +import org.glassfish.jersey.server.ResourceConfig +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.mockito.kotlin.* + +import java.time.Instant + +internal class MapperResourceEnhancerTest { + + @Test + fun getMapper() { + val mapper = MapperResourceEnhancer.createDefaultMapper() + assertThat(mapper.writeValueAsString(InstantWrapper()), equalTo("""{"date":"1970-01-01T01:00:00Z"}""")) + } + + @Test + fun enhanceResourceConfig() { + val enhancer = MapperResourceEnhancer() + val resourceConfig = mock() + enhancer.enhanceResources(resourceConfig) + verify(resourceConfig).register(check { obj -> + val context = obj as ContextResolver<*> + val mapper = context.getContext(ObjectMapper::class.java) as ObjectMapper + assertThat(mapper.writeValueAsString(InstantWrapper()), equalTo("""{"date":"1970-01-01T01:00:00Z"}""")) + }) + } + + data class InstantWrapper( + val date: Instant = Instant.ofEpochSecond(3600) + ) +} diff --git a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockDisabledAuthResourceEnhancerFactory.kt b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockDisabledAuthResourceEnhancerFactory.kt index e8c192f..1897b2b 100644 --- a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockDisabledAuthResourceEnhancerFactory.kt +++ b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockDisabledAuthResourceEnhancerFactory.kt @@ -1,15 +1,15 @@ package org.radarbase.jersey.mock import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.config.ConfigLoader -import org.radarbase.jersey.config.EnhancerFactory -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.EnhancerFactory +import org.radarbase.jersey.enhancer.Enhancers +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer class MockDisabledAuthResourceEnhancerFactory(private val config: AuthConfig) : EnhancerFactory { override fun createEnhancers(): List = listOf( MockResourceEnhancer(), - ConfigLoader.Enhancers.radar(config), - ConfigLoader.Enhancers.disabledAuthorization, - ConfigLoader.Enhancers.httpException, - ConfigLoader.Enhancers.generalException) + Enhancers.radar(config), + Enhancers.disabledAuthorization, + Enhancers.exception, + ) } diff --git a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancer.kt b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancer.kt index fa317b7..da6c4cc 100644 --- a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancer.kt +++ b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancer.kt @@ -4,13 +4,13 @@ import jakarta.inject.Singleton import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.auth.authentication.TokenValidator import org.radarbase.jersey.auth.OAuthHelper -import org.radarbase.jersey.config.ConfigLoader -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer +import org.radarbase.jersey.filter.Filters import org.radarbase.jersey.service.ProjectService class MockResourceEnhancer : JerseyResourceEnhancer { override val classes: Array> = arrayOf( - ConfigLoader.Filters.logResponse) + Filters.logResponse) override val packages: Array = arrayOf( "org.radarbase.jersey.mock.resource") diff --git a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancerFactory.kt b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancerFactory.kt index 8a02e4d..4bca52a 100644 --- a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancerFactory.kt +++ b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockResourceEnhancerFactory.kt @@ -1,18 +1,17 @@ package org.radarbase.jersey.mock import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.config.ConfigLoader -import org.radarbase.jersey.config.EnhancerFactory -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.EnhancerFactory +import org.radarbase.jersey.enhancer.Enhancers +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer class MockResourceEnhancerFactory( private val config: AuthConfig ) : EnhancerFactory { override fun createEnhancers(): List = listOf( MockResourceEnhancer(), - ConfigLoader.Enhancers.radar(config), - ConfigLoader.Enhancers.managementPortal(config), - ConfigLoader.Enhancers.httpException, - ConfigLoader.Enhancers.generalException, + Enhancers.radar(config), + Enhancers.managementPortal(config), + Enhancers.exception, ) } diff --git a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockSwaggerResourceEnhancerFactory.kt b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockSwaggerResourceEnhancerFactory.kt index 8164b52..fd292fe 100644 --- a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockSwaggerResourceEnhancerFactory.kt +++ b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/MockSwaggerResourceEnhancerFactory.kt @@ -4,9 +4,9 @@ import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info import io.swagger.v3.oas.models.info.License import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.config.ConfigLoader -import org.radarbase.jersey.config.EnhancerFactory -import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.enhancer.EnhancerFactory +import org.radarbase.jersey.enhancer.Enhancers +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import java.util.* class MockSwaggerResourceEnhancerFactory(private val config: AuthConfig) : EnhancerFactory { @@ -16,9 +16,9 @@ class MockSwaggerResourceEnhancerFactory(private val config: AuthConfig) : Enhan } return listOf( MockResourceEnhancer(), - ConfigLoader.Enhancers.radar(config), - ConfigLoader.Enhancers.disabledAuthorization, - ConfigLoader.Enhancers.swagger(OpenAPI().apply { + Enhancers.radar(config), + Enhancers.disabledAuthorization, + Enhancers.swagger(OpenAPI().apply { info = Info().apply { version = properties.getProperty("version") description = "MockProject" diff --git a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/resource/MockResource.kt b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/resource/MockResource.kt index e1dfcd9..102160f 100644 --- a/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/resource/MockResource.kt +++ b/radar-jersey/src/test/kotlin/org/radarbase/jersey/mock/resource/MockResource.kt @@ -22,6 +22,7 @@ import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.MediaType import org.radarbase.jersey.exception.HttpBadRequestException import java.io.IOException +import java.time.Instant @Path("/") @Resource @@ -73,5 +74,15 @@ class MockResource { @Path("jerseybadrequest") fun withJerseyBadRequestException(): Unit = throw BadRequestException("test") - data class DetailedUser(val accessToken: String, val name: String) + @POST + @Path("user") + fun updateUser(user: DetailedUser): DetailedUser { + return user + } + + data class DetailedUser( + val accessToken: String, + val name: String, + val createdAt: Instant = Instant.ofEpochSecond(3600), + ) }