From dea87b200fd0772252f0d3f8e2106aa679d5d841 Mon Sep 17 00:00:00 2001 From: John Niang Date: Fri, 7 Apr 2023 11:42:10 +0800 Subject: [PATCH] Add support defining custom endpoint in plugin (#3703) #### What type of PR is this? /kind feature /area core /area plugin #### What this PR does / why we need it: Including CustomEndpoint beans while building plugin router functions. After that, we can define a CustomEndpoint in plugin like this: ```java @Component public class ApplicationEndpoint implements CustomEndpoint { @Override public RouterFunction endpoint() { return SpringdocRouteBuilder.route() .GET("/applications", request -> ServerResponse.ok().build(), builder -> { builder.operationId("ListV1Alpha1Applications"); }) .build(); } @Override public GroupVersion groupVersion() { return CustomEndpoint.super.groupVersion(); } } ``` #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3700 #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../endpoint/CustomEndpointsBuilder.java | 5 +++ .../plugin/PluginCompositeRouterFunction.java | 36 ++++++++++++----- .../PluginCompositeRouterFunctionTest.java | 40 +++++++++++++++++-- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/CustomEndpointsBuilder.java b/application/src/main/java/run/halo/app/core/extension/endpoint/CustomEndpointsBuilder.java index fa1d90b268..6b8d7cf064 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/CustomEndpointsBuilder.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/CustomEndpointsBuilder.java @@ -8,6 +8,7 @@ import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; import run.halo.app.extension.GroupVersion; public class CustomEndpointsBuilder { @@ -35,6 +36,10 @@ public RouterFunction build() { .tag(gv + "/CustomEndpoint") ); }); + if (routerFunctionsMap.isEmpty()) { + // return empty route. + return request -> Mono.empty(); + } routerFunctionsMap.clear(); return routeBuilder.build(); } diff --git a/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java b/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java index 02563370c1..c5a609e6dd 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java +++ b/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java @@ -1,7 +1,9 @@ package run.halo.app.plugin; +import static run.halo.app.plugin.ExtensionContextRegistry.getInstance; + +import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerFunction; @@ -11,6 +13,8 @@ import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import run.halo.app.core.extension.endpoint.CustomEndpoint; +import run.halo.app.core.extension.endpoint.CustomEndpointsBuilder; import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry; /** @@ -44,15 +48,27 @@ public void accept(@NonNull RouterFunctions.Visitor visitor) { @SuppressWarnings("unchecked") private List> routerFunctions() { - Stream> routerFunctionStream = - ExtensionContextRegistry.getInstance().getPluginApplicationContexts() - .stream() - .flatMap(applicationContext -> applicationContext - .getBeanProvider(RouterFunction.class) - .orderedStream()) - .map(router -> (RouterFunction) router); - return Stream.concat(routerFunctionStream, - reverseProxyRouterFunctionFactory.getRouterFunctions().stream()) + var rawRouterFunctions = getInstance().getPluginApplicationContexts() + .stream() + .flatMap(applicationContext -> applicationContext + .getBeanProvider(RouterFunction.class) + .orderedStream()) + .map(router -> (RouterFunction) router) .toList(); + var reverseProxies = reverseProxyRouterFunctionFactory.getRouterFunctions(); + + var endpointBuilder = new CustomEndpointsBuilder(); + getInstance().getPluginApplicationContexts() + .forEach(context -> context.getBeanProvider(CustomEndpoint.class) + .orderedStream() + .forEach(endpointBuilder::add)); + var customEndpoint = endpointBuilder.build(); + + List> routerFunctions = + new ArrayList<>(rawRouterFunctions.size() + reverseProxies.size() + 1); + routerFunctions.addAll(rawRouterFunctions); + routerFunctions.addAll(reverseProxies); + routerFunctions.add(customEndpoint); + return routerFunctions; } } diff --git a/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java b/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java index 7906da3aa6..7a75fd8369 100644 --- a/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java +++ b/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java @@ -1,13 +1,19 @@ package run.halo.app.plugin; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; @@ -18,6 +24,7 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry; /** @@ -28,18 +35,35 @@ */ @ExtendWith(MockitoExtension.class) class PluginCompositeRouterFunctionTest { - private final ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create(); + ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create(); @Mock - private ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionRegistry; + ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionRegistry; - private PluginCompositeRouterFunction compositeRouterFunction; + @Mock + ObjectProvider rawRouterFunctionsProvider; + + @Mock + ObjectProvider customEndpointsProvider; - private HandlerFunction handlerFunction; + @InjectMocks + PluginCompositeRouterFunction compositeRouterFunction; + + HandlerFunction handlerFunction; @BeforeEach @SuppressWarnings("unchecked") void setUp() { + var fakeContext = mock(PluginApplicationContext.class); + ExtensionContextRegistry.getInstance().register("fake-plugin", fakeContext); + + when(rawRouterFunctionsProvider.orderedStream()).thenReturn(Stream.empty()); + when(customEndpointsProvider.orderedStream()).thenReturn(Stream.empty()); + + when(fakeContext.getBeanProvider(RouterFunction.class)) + .thenReturn(rawRouterFunctionsProvider); + when(fakeContext.getBeanProvider(CustomEndpoint.class)).thenReturn(customEndpointsProvider); + compositeRouterFunction = new PluginCompositeRouterFunction(reverseProxyRouterFunctionRegistry); @@ -50,6 +74,11 @@ void setUp() { .thenReturn(List.of(routerFunction)); } + @AfterEach + void cleanUp() { + ExtensionContextRegistry.getInstance().remove("fake-plugin"); + } + @Test void route() { RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction); @@ -61,6 +90,9 @@ void route() { .expectNext(handlerFunction) .expectComplete() .verify(); + + verify(rawRouterFunctionsProvider).orderedStream(); + verify(customEndpointsProvider).orderedStream(); } private ServerWebExchange createExchange(String urlTemplate) {