From 801b377c578e87e2b5eff170ce757922036e5da5 Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Fri, 16 Feb 2024 23:06:36 +0100 Subject: [PATCH] GH-42 Support beforeMatched/afterMatched handlers in annotations module --- .../routing/annotations/RoutingAnnotations.kt | 8 +++ .../annotations/ReflectiveEndpointLoader.kt | 63 +++++++++++-------- .../annotations/AnnotatedRoutingTest.kt | 4 ++ 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/routing-annotations/routing-annotated-specification/src/main/kotlin/io/javalin/community/routing/annotations/RoutingAnnotations.kt b/routing-annotations/routing-annotated-specification/src/main/kotlin/io/javalin/community/routing/annotations/RoutingAnnotations.kt index 951e22f..8951d9a 100644 --- a/routing-annotations/routing-annotated-specification/src/main/kotlin/io/javalin/community/routing/annotations/RoutingAnnotations.kt +++ b/routing-annotations/routing-annotated-specification/src/main/kotlin/io/javalin/community/routing/annotations/RoutingAnnotations.kt @@ -50,10 +50,18 @@ annotation class Options(val value: String = "", val async: Boolean = false) @Target(FUNCTION) annotation class Before(val value: String = "*", val async: Boolean = false) +@Retention(RUNTIME) +@Target(FUNCTION) +annotation class BeforeMatched(val value: String = "*", val async: Boolean = false) + @Retention(RUNTIME) @Target(FUNCTION) annotation class After(val value: String = "*", val async: Boolean = false) +@Retention(RUNTIME) +@Target(FUNCTION) +annotation class AfterMatched(val value: String = "*", val async: Boolean = false) + @Retention(RUNTIME) @Target(FUNCTION) annotation class Version(val value: String) diff --git a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt index 9e7eb60..e9f1eb6 100644 --- a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt +++ b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt @@ -1,11 +1,13 @@ package io.javalin.community.routing.annotations import io.javalin.community.routing.Route +import io.javalin.community.routing.Route.BEFORE_MATCHED import io.javalin.community.routing.dsl.DefaultDslException import io.javalin.community.routing.dsl.DefaultDslRoute import io.javalin.http.Context import io.javalin.http.HttpStatus import io.javalin.validation.Validation +import java.lang.reflect.AnnotatedElement import java.lang.reflect.Method import java.lang.reflect.Parameter import kotlin.reflect.KClass @@ -22,7 +24,7 @@ internal class ReflectiveEndpointLoader( fun loadRoutesFromEndpoint(endpoint: Any): List { val endpointClass = endpoint::class.java - val endpointPath = endpointClass.getAnnotation(Endpoints::class.java) + val endpointPath = endpointClass.getAnnotation() ?.value ?: "" @@ -30,15 +32,17 @@ internal class ReflectiveEndpointLoader( endpointClass.declaredMethods.forEach { method -> val (httpMethod, path, async) = when { - method.isAnnotationPresent(Before::class.java) -> method.getAnnotation(Before::class.java).let { Triple(Route.BEFORE, it.value, it.async) } - method.isAnnotationPresent(After::class.java) -> method.getAnnotation(After::class.java).let { Triple(Route.AFTER, it.value, it.async) } - method.isAnnotationPresent(Get::class.java) -> method.getAnnotation(Get::class.java).let { Triple(Route.GET, it.value, it.async) } - method.isAnnotationPresent(Put::class.java) -> method.getAnnotation(Put::class.java).let { Triple(Route.PUT, it.value, it.async) } - method.isAnnotationPresent(Post::class.java) -> method.getAnnotation(Post::class.java).let { Triple(Route.POST, it.value, it.async) } - method.isAnnotationPresent(Delete::class.java) -> method.getAnnotation(Delete::class.java).let { Triple(Route.DELETE, it.value, it.async) } - method.isAnnotationPresent(Head::class.java) -> method.getAnnotation(Head::class.java).let { Triple(Route.HEAD, it.value, it.async) } - method.isAnnotationPresent(Patch::class.java) -> method.getAnnotation(Patch::class.java).let { Triple(Route.PATCH, it.value, it.async) } - method.isAnnotationPresent(Options::class.java) -> method.getAnnotation(Options::class.java).let { Triple(Route.OPTIONS, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.BEFORE, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(BEFORE_MATCHED, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.AFTER, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.AFTER_MATCHED, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.GET, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.PUT, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.POST, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.DELETE, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.HEAD, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.PATCH, it.value, it.async) } + method.isAnnotationPresent() -> method.getAnnotation()!!.let { Triple(Route.OPTIONS, it.value, it.async) } else -> return@forEach } @@ -50,13 +54,13 @@ internal class ReflectiveEndpointLoader( createArgumentSupplier(it) ?: throw IllegalArgumentException("Unsupported parameter type: $it") } - val status = method.getAnnotation(Status::class.java) + val status = method.getAnnotation() val resultHandler = findResultHandler(method) val route = AnnotatedRoute( method = httpMethod, path = ("/$endpointPath/$path").replace(repeatedPathSeparatorRegex, "/"), - version = method.getAnnotation(Version::class.java)?.value, + version = method.getAnnotation()?.value, handler = { val arguments = argumentSuppliers .map { it(this, Unit) } @@ -81,7 +85,7 @@ internal class ReflectiveEndpointLoader( val dslExceptions = mutableListOf() endpointClass.declaredMethods.forEach { method -> - val exceptionHandlerAnnotation = method.getAnnotation(ExceptionHandler::class.java) ?: return@forEach + val exceptionHandlerAnnotation = method.getAnnotation() ?: return@forEach require(method.trySetAccessible()) { "Unable to access method $method in class $endpointClass" @@ -91,7 +95,7 @@ internal class ReflectiveEndpointLoader( createArgumentSupplier(it) ?: throw IllegalArgumentException("Unsupported parameter type: $it") } - val status = method.getAnnotation(Status::class.java) + val status = method.getAnnotation() val resultHandler = findResultHandler(method) val dslException = AnnotatedException( @@ -166,46 +170,55 @@ internal class ReflectiveEndpointLoader( type.isAssignableFrom(Context::class.java) -> { ctx, _ -> ctx } - isAnnotationPresent(Param::class.java) -> { ctx, _ -> - getAnnotation(Param::class.java) + isAnnotationPresent() -> { ctx, _ -> + getAnnotationOrThrow() .value .ifEmpty { name } .let { ctx.pathParamAsClass(it, type) } .get() } - isAnnotationPresent(Header::class.java) -> { ctx, _ -> - getAnnotation(Header::class.java) + isAnnotationPresent
() -> { ctx, _ -> + getAnnotationOrThrow
() .value .ifEmpty { name } .let { ctx.headerAsClass(it, type) } .get() } - isAnnotationPresent(Query::class.java) -> { ctx, _ -> - getAnnotation(Query::class.java) + isAnnotationPresent() -> { ctx, _ -> + getAnnotationOrThrow() .value .ifEmpty { name } .let { ctx.queryParamAsClass(it, type) } .get() } - isAnnotationPresent(Form::class.java) -> { ctx, _ -> - getAnnotation(Form::class.java) + isAnnotationPresent
() -> { ctx, _ -> + getAnnotationOrThrow() .value .ifEmpty { name } .let { ctx.formParamAsClass(it, type) } .get() } - isAnnotationPresent(Cookie::class.java) -> { ctx, _ -> - getAnnotation(Cookie::class.java) + isAnnotationPresent() -> { ctx, _ -> + getAnnotationOrThrow() .value .ifEmpty { name } .let { Validation().validator(it, type, ctx.cookie(it)) } .get() } - isAnnotationPresent(Body::class.java) -> { ctx, _ -> + isAnnotationPresent() -> { ctx, _ -> ctx.bodyAsClass(parameter.parameterizedType) } else -> null } } + private inline fun AnnotatedElement.isAnnotationPresent(): Boolean = + isAnnotationPresent(A::class.java) + + private inline fun AnnotatedElement.getAnnotationOrThrow(): A = + getAnnotation(A::class.java) ?: throw IllegalStateException("Annotation ${A::class.java.name} not found") + + private inline fun AnnotatedElement.getAnnotation(): A? = + getAnnotation(A::class.java) + } \ No newline at end of file diff --git a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt index 2a246d3..719e611 100644 --- a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt +++ b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt @@ -74,6 +74,8 @@ class AnnotatedRoutingTest { object { // formatter:off @Before fun beforeEach(ctx: Context) { ctx.header("before", "true") } + @BeforeMatched fun beforeEachMatched(ctx: Context) { ctx.header("before-matched", "true") } + @AfterMatched fun afterEachMatched(ctx: Context) { ctx.header("after-matched", "true") } @After fun afterEach(ctx: Context) { ctx.header("after", "true") } @Get("/get") fun testGet(ctx: Context) { ctx.header("get", "true") } @Post("/post") fun testPost(ctx: Context) { ctx.header("post", "true") } @@ -94,7 +96,9 @@ class AnnotatedRoutingTest { val response = request(it.name, "${client.origin}/test/${it.name.lowercase()}").asEmpty() assertThat(response.headers.getFirst(it.name.lowercase())).isEqualTo("true") assertThat(response.headers.getFirst("before")).isEqualTo("true") + assertThat(response.headers.getFirst("before-matched")).isEqualTo("true") assertThat(response.headers.getFirst("after")).isEqualTo("true") + assertThat(response.headers.getFirst("after-matched")).isEqualTo("true") } }