diff --git a/src/main/java/run/halo/app/config/ExtensionConfiguration.java b/src/main/java/run/halo/app/config/ExtensionConfiguration.java index 31c75de5b..f650ab4aa 100644 --- a/src/main/java/run/halo/app/config/ExtensionConfiguration.java +++ b/src/main/java/run/halo/app/config/ExtensionConfiguration.java @@ -57,7 +57,6 @@ import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.properties.HaloProperties; import run.halo.app.plugin.ExtensionComponentsFinder; import run.halo.app.plugin.HaloPluginManager; -import run.halo.app.plugin.resources.JsBundleRuleProvider; import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry; import run.halo.app.theme.router.TemplateRouteManager; @@ -116,10 +115,9 @@ public class ExtensionConfiguration { } @Bean - Controller pluginController(ExtensionClient client, HaloPluginManager haloPluginManager, - JsBundleRuleProvider jsBundleRule) { + Controller pluginController(ExtensionClient client, HaloPluginManager haloPluginManager) { return new ControllerBuilder("plugin-controller", client) - .reconciler(new PluginReconciler(client, haloPluginManager, jsBundleRule)) + .reconciler(new PluginReconciler(client, haloPluginManager)) .extension(new Plugin()) .build(); } diff --git a/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java index cf31b6fac..0988a473c 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java @@ -17,8 +17,7 @@ import run.halo.app.extension.controller.Reconciler.Request; import run.halo.app.infra.utils.JsonUtils; import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginStartingError; -import run.halo.app.plugin.resources.JsBundleRuleProvider; -import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory; +import run.halo.app.plugin.resources.BundleResourceUtils; /** * Plugin reconciler. @@ -32,14 +31,10 @@ public class PluginReconciler implements Reconciler { private final ExtensionClient client; private final HaloPluginManager haloPluginManager; - private final JsBundleRuleProvider jsBundleRule; - public PluginReconciler(ExtensionClient client, - HaloPluginManager haloPluginManager, - JsBundleRuleProvider jsBundleRule) { + HaloPluginManager haloPluginManager) { this.client = client; this.haloPluginManager = haloPluginManager; - this.jsBundleRule = jsBundleRule; } @Override @@ -129,15 +124,12 @@ public class PluginReconciler implements Reconciler { PluginState currentState = haloPluginManager.startPlugin(pluginName); handleStatus(plugin, currentState, PluginState.STARTED); Plugin.PluginStatus status = plugin.statusNonNull(); - // TODO Check whether the JS bundle rule exists. If it does not exist, do not populate - // populate stylesheet path - jsBundleRule.jsRule(pluginName) - .map(jsRule -> ReverseProxyRouterFunctionFactory.buildRoutePath(pluginName, jsRule)) - .ifPresent(status::setEntry); - jsBundleRule.cssRule(pluginName) - .map(cssRule -> ReverseProxyRouterFunctionFactory.buildRoutePath(pluginName, cssRule)) - .ifPresent(status::setStylesheet); + String jsBundlePath = BundleResourceUtils.getJsBundlePath(haloPluginManager, pluginName); + status.setEntry(jsBundlePath); + + String cssBundlePath = BundleResourceUtils.getCssBundlePath(haloPluginManager, pluginName); + status.setStylesheet(cssBundlePath); status.setLastStartTime(Instant.now()); } diff --git a/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java b/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java index ea708fd09..54027c4f9 100644 --- a/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java +++ b/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java @@ -22,8 +22,13 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; import org.springframework.util.StringUtils; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import run.halo.app.plugin.resources.BundleResourceUtils; /** * Plugin autoconfiguration for Spring Boot. @@ -150,4 +155,22 @@ public class PluginAutoConfiguration { pluginManager.setSystemVersion(pluginProperties.getSystemVersion()); return pluginManager; } + + @Bean + public RouterFunction pluginJsBundleRoute(HaloPluginManager haloPluginManager) { + return RouterFunctions.route() + .GET("/plugins/{name}/assets/console/{*resource}", request -> { + String pluginName = request.pathVariable("name"); + String fileName = request.pathVariable("resource"); + + Resource jsBundleResource = + BundleResourceUtils.getJsBundleResource(haloPluginManager, pluginName, + fileName); + if (jsBundleResource == null) { + return ServerResponse.notFound().build(); + } + return ServerResponse.ok().bodyValue(jsBundleResource); + }) + .build(); + } } diff --git a/src/main/java/run/halo/app/plugin/PluginConst.java b/src/main/java/run/halo/app/plugin/PluginConst.java index a8bb8cf6e..d497c5fa9 100644 --- a/src/main/java/run/halo/app/plugin/PluginConst.java +++ b/src/main/java/run/halo/app/plugin/PluginConst.java @@ -13,4 +13,8 @@ public interface PluginConst { String PLUGIN_NAME_LABEL_NAME = "plugin.halo.run/plugin-name"; String SYSTEM_PLUGIN_NAME = "system"; + + static String assertsRoutePrefix(String pluginName) { + return "/plugins/" + pluginName + "/assets/"; + } } diff --git a/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java b/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java new file mode 100644 index 000000000..5c4592f20 --- /dev/null +++ b/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java @@ -0,0 +1,88 @@ +package run.halo.app.plugin.resources; + +import org.pf4j.PluginWrapper; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import run.halo.app.infra.utils.PathUtils; +import run.halo.app.plugin.HaloPluginManager; +import run.halo.app.plugin.PluginConst; + +/** + * Plugin bundle resources utils. + * + * @author guqing + * @since 2.0.0 + */ +public abstract class BundleResourceUtils { + private static final String CONSOLE_BUNDLE_LOCATION = "console"; + private static final String JS_BUNDLE = "main.js"; + private static final String CSS_BUNDLE = "style.css"; + + /** + * Gets plugin css bundle resource path relative to the plugin classpath if exists. + * + * @return css bundle resource path if exists, otherwise return null. + */ + @Nullable + public static String getCssBundlePath(HaloPluginManager haloPluginManager, + String pluginName) { + Resource jsBundleResource = getJsBundleResource(haloPluginManager, pluginName, CSS_BUNDLE); + if (jsBundleResource != null) { + return consoleResourcePath(pluginName, CSS_BUNDLE); + } + return null; + } + + private static String consoleResourcePath(String pluginName, String name) { + return PathUtils.combinePath(PluginConst.assertsRoutePrefix(pluginName), + CONSOLE_BUNDLE_LOCATION, name); + } + + /** + * Gets plugin js bundle resource path relative to the plugin classpath if exists. + * + * @return js bundle resource path if exists, otherwise return null. + */ + @Nullable + public static String getJsBundlePath(HaloPluginManager haloPluginManager, + String pluginName) { + Resource jsBundleResource = getJsBundleResource(haloPluginManager, pluginName, JS_BUNDLE); + if (jsBundleResource != null) { + return consoleResourcePath(pluginName, JS_BUNDLE); + } + return null; + } + + /** + * Gets js bundle resource by plugin name in console location. + * + * @return js bundle resource if exists, otherwise null + */ + @Nullable + public static Resource getJsBundleResource(HaloPluginManager pluginManager, String pluginName, + String bundleName) { + Assert.hasText(pluginName, "The pluginName must not be blank"); + Assert.hasText(bundleName, "Bundle name must not be blank"); + + DefaultResourceLoader resourceLoader = getResourceLoader(pluginManager, pluginName); + if (resourceLoader == null) { + return null; + } + String path = PathUtils.combinePath(CONSOLE_BUNDLE_LOCATION, bundleName); + Resource resource = resourceLoader.getResource(path); + return resource.exists() ? resource : null; + } + + @Nullable + private static DefaultResourceLoader getResourceLoader(HaloPluginManager pluginManager, + String pluginName) { + Assert.notNull(pluginManager, "Plugin manager must not be null"); + PluginWrapper plugin = pluginManager.getPlugin(pluginName); + if (plugin == null) { + return null; + } + return new DefaultResourceLoader(plugin.getPluginClassLoader()); + } +} diff --git a/src/main/java/run/halo/app/plugin/resources/JsBundleRuleProvider.java b/src/main/java/run/halo/app/plugin/resources/JsBundleRuleProvider.java deleted file mode 100644 index 61b722aac..000000000 --- a/src/main/java/run/halo/app/plugin/resources/JsBundleRuleProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -package run.halo.app.plugin.resources; - -import java.util.Optional; -import org.pf4j.PluginWrapper; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider; -import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule; -import run.halo.app.plugin.HaloPluginManager; - -/** - * TODO Optimize code to support user customize js bundle rules. - * - * @author guqing - * @since 2.0.0 - */ -@Component -public class JsBundleRuleProvider { - private static final String JS_LOCATION = "/console/main.js"; - private static final String CSS_LOCATION = "/console/style.css"; - - private static final FileReverseProxyProvider JS_FILE_PROXY = - new FileReverseProxyProvider("console", "main.js"); - - private static final FileReverseProxyProvider CSS_FILE_PROXY = - new FileReverseProxyProvider("console", "style.css"); - - private final HaloPluginManager haloPluginManager; - - public JsBundleRuleProvider(HaloPluginManager haloPluginManager) { - this.haloPluginManager = haloPluginManager; - } - - /** - * Gets plugin js bundle rule. - * - * @param pluginName plugin name - * @return a js bundle rule - */ - public Optional jsRule(String pluginName) { - return Optional.of(JS_LOCATION) - .filter(path -> createResourceLoader(pluginName) - .getResource(path).exists()) - .map(path -> new ReverseProxyRule(path, JS_FILE_PROXY)); - } - - /** - * Gets plugin stylesheet rule. - * - * @param pluginName plugin name - * @return a stylesheet bundle rule - */ - public Optional cssRule(String pluginName) { - return Optional.of(CSS_LOCATION) - .filter(path -> createResourceLoader(pluginName) - .getResource(path) - .exists()) - .map(path -> new ReverseProxyRule(path, CSS_FILE_PROXY)); - } - - @NonNull - private DefaultResourceLoader createResourceLoader(String pluginName) { - PluginWrapper plugin = haloPluginManager.getPlugin(pluginName); - if (plugin == null) { - return new DefaultResourceLoader(); - } - return new DefaultResourceLoader(plugin.getPluginClassLoader()); - } - -} diff --git a/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java b/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java index 85c77e36d..584f90259 100644 --- a/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java +++ b/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java @@ -4,20 +4,19 @@ import static org.springframework.http.MediaType.ALL; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.accept; -import java.util.ArrayList; -import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; +import org.springframework.http.server.PathContainer; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; -import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.util.pattern.PathPatternParser; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.ReverseProxy; @@ -38,13 +37,6 @@ import run.halo.app.plugin.PluginConst; @Slf4j @Component public class ReverseProxyRouterFunctionFactory { - private static final String REVERSE_PROXY_API_PREFIX = "/assets"; - - private final JsBundleRuleProvider jsBundleRuleProvider; - - public ReverseProxyRouterFunctionFactory(JsBundleRuleProvider jsBundleRuleProvider) { - this.jsBundleRuleProvider = jsBundleRuleProvider; - } /** *

Create {@link RouterFunction} according to the {@link ReverseProxy} custom resource @@ -67,7 +59,7 @@ public class ReverseProxyRouterFunctionFactory { Assert.notNull(reverseProxy, "The reverseProxy must not be null."); Assert.notNull(applicationContext, "The applicationContext must not be null."); final var pluginId = getPluginId(applicationContext); - var rules = getReverseProxyRules(pluginId, reverseProxy); + var rules = getReverseProxyRules(reverseProxy); return rules.map(rule -> { String routePath = buildRoutePath(pluginId, rule); @@ -93,21 +85,13 @@ public class ReverseProxyRouterFunctionFactory { return PluginConst.SYSTEM_PLUGIN_NAME; } - private Flux getReverseProxyRules(String pluginId, - ReverseProxy reverseProxy) { - return Flux.fromIterable(reverseProxy.getRules()) - .concatWith(Flux.fromIterable(getJsBundleRules(pluginId))); - } - - private List getJsBundleRules(String pluginId) { - List rules = new ArrayList<>(2); - jsBundleRuleProvider.jsRule(pluginId).ifPresent(rules::add); - jsBundleRuleProvider.cssRule(pluginId).ifPresent(rules::add); - return rules; + private Flux getReverseProxyRules(ReverseProxy reverseProxy) { + return Flux.fromIterable(reverseProxy.getRules()); } public static String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) { - return PathUtils.combinePath(REVERSE_PROXY_API_PREFIX, pluginId, reverseProxyRule.path()); + return PathUtils.combinePath(PluginConst.assertsRoutePrefix(pluginId), + reverseProxyRule.path()); } /** @@ -140,13 +124,13 @@ public class ReverseProxyRouterFunctionFactory { if (StringUtils.isNotBlank(configuredFilename)) { filename = configuredFilename; } else { - AntPathMatcher antPathMatcher = new AntPathMatcher(); String routePath = buildRoutePath(pluginId, rule); - filename = - antPathMatcher.extractPathWithinPattern(routePath, request.path()); + PathContainer pathContainer = PathPatternParser.defaultInstance.parse(routePath) + .extractPathWithinPattern(PathContainer.parsePath(request.path())); + filename = pathContainer.value(); } - String filePath = PathUtils.appendPathSeparatorIfMissing(directory) + filename; + String filePath = PathUtils.combinePath(directory, filename); return pluginApplicationContext.getResource(filePath); } } diff --git a/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java index 8a8cea01c..1b3760ffb 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java @@ -25,7 +25,6 @@ import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.utils.JsonUtils; import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginStartingError; -import run.halo.app.plugin.resources.JsBundleRuleProvider; /** * Tests for {@link PluginReconciler}. @@ -49,8 +48,7 @@ class PluginReconcilerTest { @BeforeEach void setUp() { - JsBundleRuleProvider jsBundleRule = new JsBundleRuleProvider(haloPluginManager); - pluginReconciler = new PluginReconciler(extensionClient, haloPluginManager, jsBundleRule); + pluginReconciler = new PluginReconciler(extensionClient, haloPluginManager); when(haloPluginManager.getPlugin(any())).thenReturn(pluginWrapper); when(haloPluginManager.getUnresolvedPlugins()).thenReturn(List.of()); diff --git a/src/test/java/run/halo/app/infra/utils/PathUtilsTest.java b/src/test/java/run/halo/app/infra/utils/PathUtilsTest.java index fc12d43ea..5e53778bc 100644 --- a/src/test/java/run/halo/app/infra/utils/PathUtilsTest.java +++ b/src/test/java/run/halo/app/infra/utils/PathUtilsTest.java @@ -21,6 +21,9 @@ class PathUtilsTest { String s = PathUtils.combinePath(segments.split(",")); assertThat(s).isEqualTo(expected); }); + + String s = PathUtils.combinePath("a", "", "c"); + assertThat(s).isEqualTo("/a/c"); } private Map getCombinePathCases() { diff --git a/src/test/java/run/halo/app/plugin/resources/BundleResourceUtilsTest.java b/src/test/java/run/halo/app/plugin/resources/BundleResourceUtilsTest.java new file mode 100644 index 000000000..c703dd478 --- /dev/null +++ b/src/test/java/run/halo/app/plugin/resources/BundleResourceUtilsTest.java @@ -0,0 +1,81 @@ +package run.halo.app.plugin.resources; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +import java.net.MalformedURLException; +import java.net.URL; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.pf4j.PluginClassLoader; +import org.pf4j.PluginWrapper; +import org.springframework.core.io.Resource; +import run.halo.app.plugin.HaloPluginManager; + +/** + * Tests for {@link BundleResourceUtils}. + * + * @author guqing + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +class BundleResourceUtilsTest { + + @Mock + private HaloPluginManager pluginManager; + + @BeforeEach + void setUp() throws MalformedURLException { + PluginWrapper pluginWrapper = Mockito.mock(PluginWrapper.class); + PluginClassLoader pluginClassLoader = Mockito.mock(PluginClassLoader.class); + when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader); + lenient().when(pluginManager.getPlugin(eq("fake-plugin"))).thenReturn(pluginWrapper); + + lenient().when(pluginClassLoader.getResource(eq("console/main.js"))).thenReturn( + new URL("file://console/main.js")); + lenient().when(pluginClassLoader.getResource(eq("console/style.css"))).thenReturn( + new URL("file://console/style.css")); + } + + @Test + void getCssBundlePath() { + String cssBundlePath = + BundleResourceUtils.getCssBundlePath(pluginManager, "nothing-plugin"); + assertThat(cssBundlePath).isNull(); + + cssBundlePath = BundleResourceUtils.getCssBundlePath(pluginManager, "fake-plugin"); + assertThat(cssBundlePath).isEqualTo("/plugins/fake-plugin/assets/console/style.css"); + } + + @Test + void getJsBundlePath() { + String jsBundlePath = + BundleResourceUtils.getJsBundlePath(pluginManager, "nothing-plugin"); + assertThat(jsBundlePath).isNull(); + + jsBundlePath = BundleResourceUtils.getJsBundlePath(pluginManager, "fake-plugin"); + assertThat(jsBundlePath).isEqualTo("/plugins/fake-plugin/assets/console/main.js"); + } + + @Test + void getJsBundleResource() { + Resource jsBundleResource = + BundleResourceUtils.getJsBundleResource(pluginManager, "fake-plugin", "main.js"); + assertThat(jsBundleResource).isNotNull(); + assertThat(jsBundleResource.exists()).isTrue(); + + jsBundleResource = + BundleResourceUtils.getJsBundleResource(pluginManager, "fake-plugin", "test.js"); + assertThat(jsBundleResource).isNull(); + + jsBundleResource = + BundleResourceUtils.getJsBundleResource(pluginManager, "nothing-plugin", "main.js"); + assertThat(jsBundleResource).isNull(); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java b/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java index e74a20a29..6f6bc0507 100644 --- a/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java +++ b/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java @@ -12,7 +12,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import reactor.test.StepVerifier; import run.halo.app.core.extension.ReverseProxy; import run.halo.app.extension.Metadata; -import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginApplicationContext; import run.halo.app.plugin.PluginConst; @@ -27,16 +26,13 @@ class ReverseProxyRouterFunctionFactoryTest { @Mock private PluginApplicationContext pluginApplicationContext; - @Mock - private HaloPluginManager haloPluginManager; private ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory; @BeforeEach void setUp() { - JsBundleRuleProvider jsBundleRuleProvider = new JsBundleRuleProvider(haloPluginManager); reverseProxyRouterFunctionFactory = - new ReverseProxyRouterFunctionFactory(jsBundleRuleProvider); + new ReverseProxyRouterFunctionFactory(); when(pluginApplicationContext.getPluginId()).thenReturn("fakeA"); } @@ -50,7 +46,6 @@ class ReverseProxyRouterFunctionFactoryTest { .verifyComplete(); } - private ReverseProxy mockReverseProxy() { ReverseProxy.ReverseProxyRule reverseProxyRule = new ReverseProxy.ReverseProxyRule("/static/**",