diff --git a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/AnnotatedEndpointMetadata.kt b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/AnnotatedEndpointMetadata.kt new file mode 100644 index 0000000..8d831ee --- /dev/null +++ b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/AnnotatedEndpointMetadata.kt @@ -0,0 +1,15 @@ +package io.javalin.community.routing.annotations + +import io.javalin.community.routing.dsl.DslRouteMetadata +import java.util.* + +class AnnotatedEndpointMetadata( + val annotations: Set +) : DslRouteMetadata { + + fun getAnnotation(annotationClass: Class): Optional = + annotations + .firstOrNull { it.annotationClass.java == annotationClass } + .let { Optional.ofNullable(it) } + +} \ No newline at end of file diff --git a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/AnnotatedRouting.kt b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/AnnotatedRouting.kt index 9406bbe..6f95686 100644 --- a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/AnnotatedRouting.kt +++ b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/AnnotatedRouting.kt @@ -2,6 +2,7 @@ package io.javalin.community.routing.annotations import io.javalin.community.routing.Route import io.javalin.community.routing.dsl.DslRoute +import io.javalin.community.routing.dsl.DslRouteMetadataFactory import io.javalin.community.routing.invokeAsSamWithReceiver import io.javalin.community.routing.registerRoute import io.javalin.community.routing.sortRoutes @@ -93,17 +94,25 @@ object AnnotatedRouting : RoutingApiInitializer { .sortRoutes() .groupBy { RouteIdentifier(it.method, it.path) } .map { (id, routes) -> - id to when (routes.size) { - 1 -> routes.first().let { Handler { ctx -> it.handler(ctx) } } - else -> createVersionedRoute( - apiVersionHeader = configuration.apiVersionHeader, - id = id, - routes = routes - ) + when (routes.size) { + 1 -> Triple(id, routes.first().let { Handler { ctx -> it.handler(ctx) } }, routes.first().metadataFactory) + else -> + createVersionedRoute( + apiVersionHeader = configuration.apiVersionHeader, + id = id, + routes = routes + ).let { + Triple(id, it.first, it.second) + } } } - .forEach { (id, handler) -> - internalRouter.registerRoute(id.route, id.path, handler) + .forEach { (id, handler, metadataFactory) -> + internalRouter.registerRoute( + route = id.route, + path = id.path, + handler = handler, + metadata = metadataFactory?.let { arrayOf(it) } ?: emptyArray(), + ) } registeredExceptionHandlers.forEach { annotatedException -> @@ -113,11 +122,19 @@ object AnnotatedRouting : RoutingApiInitializer { } } - private fun createVersionedRoute(apiVersionHeader: String, id: RouteIdentifier, routes: List>): Handler { + private fun createVersionedRoute(apiVersionHeader: String, id: RouteIdentifier, routes: List>): Pair { val versions = routes.map { it.version } check(versions.size == versions.toSet().size) { "Duplicated version found for the same route: ${id.route} ${id.path} (versions: $versions)" } - return Handler { ctx -> + val metadataFactory = DslRouteMetadataFactory { ctx -> + val version = ctx.header(apiVersionHeader) + + routes.firstOrNull { it.version == version } + ?.let { it.metadataFactory?.create(ctx) } + ?: throw BadRequestResponse("This endpoint does not support the requested API version ($version).") + } + + val handler = Handler { ctx -> val version = ctx.header(apiVersionHeader) routes.firstOrNull { it.version == version } @@ -125,6 +142,8 @@ object AnnotatedRouting : RoutingApiInitializer { ?.invoke(ctx) ?: throw BadRequestResponse("This endpoint does not support the requested API version ($version).") } + + return handler to metadataFactory } } \ No newline at end of file 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 acf8463..f8bbb25 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 @@ -4,6 +4,7 @@ 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.community.routing.dsl.DslRouteMetadataFactory import io.javalin.event.JavalinLifecycleEvent import io.javalin.http.Context import io.javalin.http.HttpStatus @@ -60,7 +61,9 @@ internal class ReflectiveEndpointLoader( "Unable to access method $method in class $endpointClass" } + val annotations = method.annotations.toSet() val types = method.genericParameterTypes + val argumentSuppliers = method.parameters.mapIndexed { idx, parameter -> createArgumentSupplier(parameter, types[idx]) ?: throw IllegalArgumentException("Unsupported parameter type: $parameter") } @@ -80,6 +83,7 @@ internal class ReflectiveEndpointLoader( AnnotatedRoute( method = httpMethod, path = pathToAdd, + metadataFactory = { _ -> AnnotatedEndpointMetadata(annotations = annotations) }, version = method.getAnnotation()?.value, handler = { val arguments = argumentSuppliers @@ -231,6 +235,14 @@ internal class ReflectiveEndpointLoader( .firstOrNull() ?.endpoint } + expectedTypeAsClass.isAssignableFrom(AnnotatedEndpointMetadata::class.java) -> { ctx, _ -> + internalRouter + .findHttpHandlerEntries(ctx.method(), ctx.path().removePrefix(ctx.contextPath())) + .firstOrNull() + ?.endpoint + ?.metadata(DslRouteMetadataFactory::class.java) + ?.create(ctx) + } isAnnotationPresent() -> { ctx, _ -> getAnnotationOrThrow() .value 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 c5126a1..d6d7803 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 @@ -6,6 +6,7 @@ import io.javalin.community.routing.annotations.AnnotatedRouting.Annotated import io.javalin.event.JavalinLifecycleEvent.SERVER_STARTED import io.javalin.http.Context import io.javalin.http.HandlerType +import io.javalin.http.Header.AUTHORIZATION import io.javalin.http.HttpStatus import io.javalin.router.Endpoint import io.javalin.testtools.HttpClient @@ -542,4 +543,43 @@ class AnnotatedRoutingTest { } + @Retention(AnnotationRetention.RUNTIME) + annotation class Protected + + @Nested + inner class Metadata { + + @Test + fun `should expose custom annotations in metadata`() = + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + object { + @BeforeMatched + fun protect(ctx: Context, metadata: AnnotatedEndpointMetadata) { + val isProtected = metadata.getAnnotation(Protected::class.java).isPresent + val hasValidToken = ctx.header(AUTHORIZATION) == "Bearer token" + + if (isProtected && !hasValidToken) { + ctx.status(HttpStatus.UNAUTHORIZED).skipRemainingHandlers() + } + } + + @Protected + @Get("/protected") + fun endpoint(ctx: Context) { + ctx.result("success") + } + } + ) + } + } + ) { _, client -> + assertThat(Unirest.get("${client.origin}/protected").asString().status).isEqualTo(HttpStatus.UNAUTHORIZED.code) + assertThat(Unirest.get("${client.origin}/protected").header(AUTHORIZATION, "Bearer token").asString().body).isEqualTo("success") + } + + } + } \ No newline at end of file diff --git a/routing-core/src/main/kotlin/io/javalin/community/routing/JavalinRoutingExtensions.kt b/routing-core/src/main/kotlin/io/javalin/community/routing/JavalinRoutingExtensions.kt index 6835090..d14b887 100644 --- a/routing-core/src/main/kotlin/io/javalin/community/routing/JavalinRoutingExtensions.kt +++ b/routing-core/src/main/kotlin/io/javalin/community/routing/JavalinRoutingExtensions.kt @@ -4,11 +4,8 @@ import io.javalin.Javalin import io.javalin.config.RouterConfig import io.javalin.http.Handler import io.javalin.http.HandlerType -import io.javalin.router.Endpoint -import io.javalin.router.InternalRouter -import io.javalin.router.JavalinDefaultRouting -import io.javalin.router.RoutingApiInitializer -import io.javalin.router.RoutingSetupScope +import io.javalin.router.* +import io.javalin.security.Roles import io.javalin.security.RouteRole import java.util.function.Consumer @@ -43,24 +40,33 @@ data class HandlerEntry @JvmOverloads constructor( ) : Routed fun InternalRouter.registerRoute(handlerEntry: HandlerEntry) = - registerRoute(handlerEntry.route, handlerEntry.path, handlerEntry.handler, *handlerEntry.roles.toTypedArray()) + registerRoute( + route = handlerEntry.route, + path = handlerEntry.path, + handler = handlerEntry.handler, + metadata = arrayOf(Roles(handlerEntry.roles.toSet())), + ) -fun InternalRouter.registerRoute(route: Route, path: String, handler: Handler, vararg roles: RouteRole) { +@Suppress("DEPRECATION") +fun InternalRouter.registerRoute(route: Route, path: String, handler: Handler, vararg metadata: EndpointMetadata) { when (route) { - Route.HEAD -> addHttpEndpoint(Endpoint(method = HandlerType.HEAD, path = path, handler = handler, roles = roles)) - Route.PATCH -> addHttpEndpoint(Endpoint(method = HandlerType.PATCH, path = path, handler = handler, roles = roles)) - Route.OPTIONS -> addHttpEndpoint(Endpoint(method = HandlerType.OPTIONS, path = path, handler = handler, roles = roles)) - Route.GET -> addHttpEndpoint(Endpoint(method = HandlerType.GET, path = path, handler = handler, roles = roles)) - Route.PUT -> addHttpEndpoint(Endpoint(method = HandlerType.PUT, path = path, handler = handler, roles = roles)) - Route.POST -> addHttpEndpoint(Endpoint(method = HandlerType.POST, path = path, handler = handler, roles = roles)) - Route.DELETE -> addHttpEndpoint(Endpoint(method = HandlerType.DELETE, path = path, handler = handler, roles = roles)) - Route.BEFORE -> addHttpEndpoint(Endpoint(method = HandlerType.BEFORE, path = path, handler = handler)) - Route.BEFORE_MATCHED -> addHttpEndpoint(Endpoint(method = HandlerType.BEFORE_MATCHED, path = path, handler = handler)) - Route.AFTER -> addHttpEndpoint(Endpoint(method = HandlerType.AFTER, path = path, handler = handler)) - Route.AFTER_MATCHED -> addHttpEndpoint(Endpoint(method = HandlerType.AFTER_MATCHED, path = path, handler = handler)) + Route.HEAD -> addHttpEndpoint(Endpoint.create(HandlerType.HEAD, path).metadata(*metadata).handler(handler)) + Route.PATCH -> addHttpEndpoint(Endpoint.create(HandlerType.PATCH, path).metadata(*metadata).handler(handler)) + Route.OPTIONS -> addHttpEndpoint(Endpoint.create(HandlerType.OPTIONS, path).metadata(*metadata).handler(handler)) + Route.GET -> addHttpEndpoint(Endpoint.create(HandlerType.GET, path).metadata(*metadata).handler(handler)) + Route.PUT -> addHttpEndpoint(Endpoint.create(HandlerType.PUT, path).metadata(*metadata).handler(handler)) + Route.POST -> addHttpEndpoint(Endpoint.create(HandlerType.POST, path).metadata(*metadata).handler(handler)) + Route.DELETE -> addHttpEndpoint(Endpoint.create(HandlerType.DELETE, path).metadata(*metadata).handler(handler)) + Route.BEFORE -> addHttpEndpoint(Endpoint.create(HandlerType.BEFORE, path).metadata(*metadata).handler(handler)) + Route.BEFORE_MATCHED -> addHttpEndpoint(Endpoint.create(HandlerType.BEFORE_MATCHED, path).metadata(*metadata).handler(handler)) + Route.AFTER -> addHttpEndpoint(Endpoint.create(HandlerType.AFTER, path).metadata(*metadata).handler(handler)) + Route.AFTER_MATCHED -> addHttpEndpoint(Endpoint.create(HandlerType.AFTER_MATCHED, path).metadata(*metadata).handler(handler)) } } +fun Endpoint.Companion.EndpointBuilder.metadata(vararg metadata: EndpointMetadata): Endpoint.Companion.EndpointBuilder = + metadata.fold(this) { acc, value -> acc.addMetadata(value) } + fun RouterConfig.mount(setup: RoutingSetupScope): RouterConfig = also { mount(Consumer { setup.invokeAsSamWithReceiver(it) diff --git a/routing-dsl/src/main/kotlin/io/javalin/community/routing/dsl/DslRoute.kt b/routing-dsl/src/main/kotlin/io/javalin/community/routing/dsl/DslRoute.kt index 38cebca..b8a36de 100644 --- a/routing-dsl/src/main/kotlin/io/javalin/community/routing/dsl/DslRoute.kt +++ b/routing-dsl/src/main/kotlin/io/javalin/community/routing/dsl/DslRoute.kt @@ -2,20 +2,30 @@ package io.javalin.community.routing.dsl import io.javalin.community.routing.Route import io.javalin.community.routing.Routed +import io.javalin.http.Context +import io.javalin.router.EndpointMetadata /* Regular routes */ typealias DslHandler = CONTEXT.() -> RESPONSE +interface DslRouteMetadataFactory : EndpointMetadata { + fun create(ctx: Context): DslRouteMetadata +} + +interface DslRouteMetadata + interface DslRoute : Routed { val method: Route val version: String? + val metadataFactory: DslRouteMetadataFactory? val handler: DslHandler } open class DefaultDslRoute( override val method: Route, override val path: String, + override val metadataFactory: DslRouteMetadataFactory? = null, override val version: String? = null, override val handler: CONTEXT.() -> RESPONSE ) : DslRoute