diff --git a/README.md b/README.md index 94fd2c8..187e2c8 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,26 @@ class Router( ) } ``` +### Serverless components - Spring Cloud Function support + +If you want to deploy your application on a serverless platform such as AWS Lambda or Azure Functions you can easily do that with the Spring Cloud Function support. + +Just add the dependency `implementation("org.springframework.cloud:spring-cloud-function-context")` to your build.gradle.kts. + +Create a @ViewComponent that implements the functional interface `Supplier`. Instead of the render() function we will now override the get method of the Supplier interface. + +If you start your application the component should be automatically rendered on http://localhost:8080 +```kotlin +@ViewComponent +class HomeViewComponent( + private val exampleService: ExampleService, +) : Supplier { + override fun get() = ViewContext( + "helloWorld" toProperty exampleService.getHelloWorld(), + "coffee" toProperty exampleService.getCoffee() + ) +} +``` ### Local Development @@ -347,6 +367,38 @@ public class Router { } ``` +### Serverless components - Spring Cloud Function support + +If you want to deploy your application on a serverless platform such as AWS Lambda or Azure Functions you can easily do that with the Spring Cloud Function support. + +Just add the dependency `implementation("org.springframework.cloud:spring-cloud-function-context")` to your build.gradle.kts. + +Create a @ViewComponent that implements the functional interface `Supplier`. Instead of the render() function we will now override the get method of the Supplier interface. + +If you start your application the component should be automatically rendered on http://localhost:8080 + + +```java +// HomeViewComponent.java +@ViewComponent +public class HomeViewComponent implements Supplier { + private final ExampleService exampleService; + + public HomeViewComponent(ExampleService exampleService) { + this.exampleService = exampleService; + } + + @Override + public ViewContext get() { + return new ViewContext( + ViewProperty.of("helloWorld", "Hello World"), + ViewProperty.of("coffee", exampleService.getCoffee()), + ViewProperty.of("office", exampleService.getOfficeHours()) + ); + } +} +``` + ### Local Development To enable live reload of the components on each save without rebuilding the application add this configuration: diff --git a/build.gradle.kts b/build.gradle.kts index 2468fec..735dc17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-aop") implementation("org.springframework.boot:spring-boot-devtools") + implementation("io.projectreactor:reactor-core") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") diff --git a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponent.kt b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponent.kt index 0c79617..d376620 100644 --- a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponent.kt +++ b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponent.kt @@ -3,6 +3,7 @@ package de.tschuehly.thymeleafviewcomponent import org.aspectj.lang.ProceedingJoinPoint import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut import org.springframework.stereotype.Component @@ -14,7 +15,17 @@ annotation class ViewComponent @Aspect @Component class ViewComponentAspect { - @Around("execution(* render(..)) && @within(de.tschuehly.thymeleafviewcomponent.ViewComponent)") + @Pointcut("@within(de.tschuehly.thymeleafviewcomponent.ViewComponent)") + fun isViewComponent(){ + // + } + + @Pointcut("execution(* render(..)) || execution(* get(..))") + fun isRenderOrGetMethod(){ + // + } + + @Around("isViewComponent() && isRenderOrGetMethod()") fun renderInject(joinPoint: ProceedingJoinPoint): ViewContext { val returnValue = joinPoint.proceed() diff --git a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentMvcConfigurer.kt b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentMvcConfigurer.kt index 4f5e816..8899627 100644 --- a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentMvcConfigurer.kt +++ b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentMvcConfigurer.kt @@ -6,10 +6,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Component class ViewComponentMvcConfigurer( - private val viewContextContainerMethodReturnValueHandler: ViewContextContainerMethodReturnValueHandler + private val viewContextContainerMethodReturnValueHandler: ViewContextContainerMethodReturnValueHandler, ) : WebMvcConfigurer { override fun addReturnValueHandlers(handlers: MutableList) { + handlers.add(ViewContextAsyncHandlerMethodReturnValueHandler()) handlers.add(viewContextContainerMethodReturnValueHandler) handlers.add(ViewContextMethodReturnValueHandler()) super.addReturnValueHandlers(handlers) diff --git a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentProcessor.kt b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentProcessor.kt index 1131d19..6fcbb16 100644 --- a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentProcessor.kt +++ b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewComponentProcessor.kt @@ -60,7 +60,13 @@ class ViewComponentProcessor(dialectPrefix: String) : val viewContext = try { expression.execute(webContext) as ViewContext } catch (e: TemplateProcessingException) { - throw ViewComponentProcessingException(e.message, e.cause) + try{ + val supplierExpression = parser.parseExpression(webContext,expressionString.replace(".render(",".get(")) + supplierExpression.execute(webContext) as ViewContext + }catch (e: TemplateProcessingException){ + throw ViewComponentProcessingException(e.message, e.cause) + } + } val engine = appCtx.getBean(SpringTemplateEngine::class.java) diff --git a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewContextAsyncHandlerMethodReturnValueHandler.kt b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewContextAsyncHandlerMethodReturnValueHandler.kt new file mode 100644 index 0000000..fa6c6f1 --- /dev/null +++ b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewContextAsyncHandlerMethodReturnValueHandler.kt @@ -0,0 +1,40 @@ +package de.tschuehly.thymeleafviewcomponent + +import org.springframework.core.MethodParameter +import org.springframework.http.ResponseEntity +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler +import org.springframework.web.method.support.ModelAndViewContainer +import reactor.core.publisher.Mono + +class ViewContextAsyncHandlerMethodReturnValueHandler() : AsyncHandlerMethodReturnValueHandler { + override fun supportsReturnType(returnType: MethodParameter): Boolean { + return Mono::class.java.isAssignableFrom(returnType.parameterType) + } + + override fun handleReturnValue( + returnValue: Any?, + returnType: MethodParameter, + mavContainer: ModelAndViewContainer, + webRequest: NativeWebRequest + ) { + if(returnValue == null){ + throw Exception("Return Value is null") + } + val mono= returnValue.takeIf { + (Mono::class.java.isAssignableFrom(it.javaClass)) + } as Mono<*> + val responseEntity = mono.block()?.takeIf { + ResponseEntity::class.java.isAssignableFrom(it.javaClass) + } as ResponseEntity<*> + val viewContext = responseEntity.body?.takeIf { + ViewContext::class.java.isAssignableFrom(it.javaClass) + } as ViewContext + mavContainer.view = viewContext.componentTemplate + mavContainer.addAllAttributes(viewContext.contextAttributes.toMap()) + } + + override fun isAsyncReturnValue(returnValue: Any?, returnType: MethodParameter): Boolean { + return returnValue != null && supportsReturnType(returnType) + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewContextMethodReturnValueHandler.kt b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewContextMethodReturnValueHandler.kt index c73c125..3f1aec6 100644 --- a/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewContextMethodReturnValueHandler.kt +++ b/src/main/kotlin/de/tschuehly/thymeleafviewcomponent/ViewContextMethodReturnValueHandler.kt @@ -18,7 +18,9 @@ class ViewContextMethodReturnValueHandler : HandlerMethodReturnValueHandler { mavContainer: ModelAndViewContainer, webRequest: NativeWebRequest ) { - val viewContext = returnValue as ViewContext + val viewContext = returnValue?.takeIf { + (ViewContext::class.java.isAssignableFrom(it.javaClass)) + } as ViewContext mavContainer.view = viewContext.componentTemplate mavContainer.addAllAttributes(viewContext.contextAttributes.toMap()) }