From 83d668b0a99511c5db0fe009d80383a5dfe6d5af Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Tue, 14 Jun 2022 23:38:12 +0800 Subject: [PATCH] feat: add plugin unstructured resource loader (#2154) * feat: add plugin unstructured resource loader * feat: add test unit case --- .../halo/app/plugin/HaloPluginManager.java | 2 - ...nInitializationLoadOnApplicationReady.java | 27 ++++++ .../PluginUnstructuredResourceLoader.java | 86 ++++++++++++++++++ .../PluginUnstructuredResourceLoaderTest.java | 36 ++++++++ .../test-unstructured-resource-loader.jar | Bin 0 -> 16472 bytes 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/main/java/run/halo/app/plugin/PluginInitializationLoadOnApplicationReady.java create mode 100644 src/main/java/run/halo/app/plugin/PluginUnstructuredResourceLoader.java create mode 100644 src/test/java/run/halo/app/plugin/PluginUnstructuredResourceLoaderTest.java create mode 100644 src/test/resources/plugin/test-unstructured-resource-loader.jar diff --git a/src/main/java/run/halo/app/plugin/HaloPluginManager.java b/src/main/java/run/halo/app/plugin/HaloPluginManager.java index 642d48737..ad0773dae 100644 --- a/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -82,8 +82,6 @@ public class HaloPluginManager extends DefaultPluginManager @Override public void afterPropertiesSet() { - // This method load, start plugins and inject extensions in Spring - loadPlugins(); this.pluginApplicationInitializer = new PluginApplicationInitializer(this); this.requestMappingManager = diff --git a/src/main/java/run/halo/app/plugin/PluginInitializationLoadOnApplicationReady.java b/src/main/java/run/halo/app/plugin/PluginInitializationLoadOnApplicationReady.java new file mode 100644 index 000000000..e3c6092f4 --- /dev/null +++ b/src/main/java/run/halo/app/plugin/PluginInitializationLoadOnApplicationReady.java @@ -0,0 +1,27 @@ +package run.halo.app.plugin; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +/** + * Load plugins after application ready. + * + * @author guqing + * @since 2.0.0 + */ +@Component +public class PluginInitializationLoadOnApplicationReady + implements ApplicationListener { + + private final HaloPluginManager haloPluginManager; + + public PluginInitializationLoadOnApplicationReady(HaloPluginManager haloPluginManager) { + this.haloPluginManager = haloPluginManager; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + haloPluginManager.loadPlugins(); + } +} diff --git a/src/main/java/run/halo/app/plugin/PluginUnstructuredResourceLoader.java b/src/main/java/run/halo/app/plugin/PluginUnstructuredResourceLoader.java new file mode 100644 index 000000000..2a72b19a6 --- /dev/null +++ b/src/main/java/run/halo/app/plugin/PluginUnstructuredResourceLoader.java @@ -0,0 +1,86 @@ +package run.halo.app.plugin; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import org.pf4j.PluginRuntimeException; +import org.pf4j.PluginWrapper; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.lang.NonNull; +import run.halo.app.extension.Unstructured; +import run.halo.app.infra.utils.YamlUnstructuredLoader; + +/** + * Plug in unstructured data loader. + * TODO Rename this class to an appropriate name. + * + * @author guqing + * @see YamlUnstructuredLoader + * @see PluginWrapper + * @see DefaultResourceLoader + * @since 2.0.0 + */ +public class PluginUnstructuredResourceLoader { + private static final String DEFAULT_RESOURCE_LOCATION = "extensions/"; + private final String resourceLocation; + + public PluginUnstructuredResourceLoader() { + resourceLocation = DEFAULT_RESOURCE_LOCATION; + } + + public PluginUnstructuredResourceLoader(String resourceLocation) { + this.resourceLocation = resourceLocation; + } + + /** + * Loading unstructured yaml configuration files in plugins. + * + * @param pluginWrapper Wrapper object holding plugin data + * @return a collection of {@link Unstructured} data(never null) + */ + @NonNull + public List loadUnstructured(PluginWrapper pluginWrapper) { + List unstructuredFilePaths = + getUnstructuredFilePathFromJar(pluginWrapper.getPluginPath()); + + DefaultResourceLoader resourceLoader = + new DefaultResourceLoader(pluginWrapper.getPluginClassLoader()); + Resource[] resources = unstructuredFilePaths.stream() + .map(resourceLoader::getResource) + .filter(Resource::exists) + .toArray(Resource[]::new); + + YamlUnstructuredLoader yamlUnstructuredLoader = new YamlUnstructuredLoader(resources); + return yamlUnstructuredLoader.load(); + } + + /** + *

Lists the path of the unstructured yaml configuration file from the plugin jar.

+ * + * @param jarPath plugin jar path + * @return Unstructured file paths relative to plugin classpath + * @throws PluginRuntimeException If loading the file fails + */ + public List getUnstructuredFilePathFromJar(Path jarPath) { + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + return jarFile.stream() + .filter(jarEntry -> { + String name = jarEntry.getName(); + return name.startsWith(resourceLocation) + && !jarEntry.isDirectory() + && isYamlFile(name); + }) + .map(ZipEntry::getName) + .toList(); + } catch (IOException e) { + throw new PluginRuntimeException(e); + } + } + + private boolean isYamlFile(String path) { + return path.endsWith(".yaml") || path.endsWith(".yml"); + } +} diff --git a/src/test/java/run/halo/app/plugin/PluginUnstructuredResourceLoaderTest.java b/src/test/java/run/halo/app/plugin/PluginUnstructuredResourceLoaderTest.java new file mode 100644 index 000000000..ec3e2f2e5 --- /dev/null +++ b/src/test/java/run/halo/app/plugin/PluginUnstructuredResourceLoaderTest.java @@ -0,0 +1,36 @@ +package run.halo.app.plugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.util.ResourceUtils; + +/** + * Tests for {@link PluginUnstructuredResourceLoader}. + * + * @author guqing + * @since 2.0.0 + */ +class PluginUnstructuredResourceLoaderTest { + + private PluginUnstructuredResourceLoader unstructuredResourceLoader; + + @BeforeEach + void setUp() { + unstructuredResourceLoader = new PluginUnstructuredResourceLoader(); + } + + @Test + void getUnstructuredFilePathFromJar() throws FileNotFoundException { + File file = ResourceUtils.getFile("classpath:plugin/test-unstructured-resource-loader.jar"); + List unstructuredFilePathFromJar = + unstructuredResourceLoader.getUnstructuredFilePathFromJar(file.toPath()); + assertThat(unstructuredFilePathFromJar).hasSize(3); + assertThat(unstructuredFilePathFromJar).contains("extensions/roles.yaml", + "extensions/reverseProxy.yaml", "extensions/test.yml"); + } +} \ No newline at end of file diff --git a/src/test/resources/plugin/test-unstructured-resource-loader.jar b/src/test/resources/plugin/test-unstructured-resource-loader.jar new file mode 100644 index 0000000000000000000000000000000000000000..bca38cec250ed5e78afcc9e07acebba67c45305f GIT binary patch literal 16472 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 literal 0 HcmV?d00001