diff --git a/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java b/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java index afd3c0e64..ff9f449b5 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java +++ b/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java @@ -5,19 +5,17 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.locks.StampedLock; +import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; import org.pf4j.AbstractExtensionFinder; import org.pf4j.PluginManager; +import org.pf4j.PluginState; +import org.pf4j.PluginStateEvent; import org.pf4j.PluginWrapper; import org.pf4j.processor.ExtensionStorage; -import org.springframework.util.Assert; /** *

The spring component finder. it will read {@code META-INF/plugin-components.idx} file in @@ -31,53 +29,41 @@ import org.springframework.util.Assert; @Slf4j public class SpringComponentsFinder extends AbstractExtensionFinder { public static final String EXTENSIONS_RESOURCE = "META-INF/plugin-components.idx"; - private final StampedLock entryStampedLock = new StampedLock(); public SpringComponentsFinder(PluginManager pluginManager) { super(pluginManager); + entries = new ConcurrentHashMap<>(); } @Override public Map> readClasspathStorages() { - log.debug("Reading components storages from classpath"); - return Collections.emptyMap(); + throw new UnsupportedOperationException(); } @Override public Map> readPluginsStorages() { - // We have to copy the source code from `org.pf4j.LegacyExtensionFinder.readPluginsStorages` - // because we have to adapt to the new extensions resource location - // `META-INF/plugin-components.idx`. - log.debug("Reading components storages from plugins"); - Map> result = new LinkedHashMap<>(); + throw new UnsupportedOperationException(); + } - List plugins = pluginManager.getPlugins(); - for (PluginWrapper plugin : plugins) { - String pluginId = plugin.getDescriptor().getPluginId(); - log.debug("Reading extensions storage from plugin '{}'", pluginId); - Set bucket = new HashSet<>(); - - try { - log.debug("Read '{}'", EXTENSIONS_RESOURCE); - ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); - try (var resourceStream = - pluginClassLoader.getResourceAsStream(EXTENSIONS_RESOURCE)) { - if (resourceStream == null) { - log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE); - } else { - collectExtensions(resourceStream, bucket); - } + private Set readPluginStorage(PluginWrapper pluginWrapper) { + var pluginId = pluginWrapper.getPluginId(); + log.debug("Reading extensions storage from plugin '{}'", pluginId); + var bucket = new HashSet(); + try { + log.debug("Read '{}'", EXTENSIONS_RESOURCE); + var classLoader = pluginWrapper.getPluginClassLoader(); + try (var resourceStream = classLoader.getResourceAsStream(EXTENSIONS_RESOURCE)) { + if (resourceStream == null) { + log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE); + } else { + collectExtensions(resourceStream, bucket); } - - debugExtensions(bucket); - - result.put(pluginId, bucket); - } catch (IOException e) { - log.error("Failed to read components from " + EXTENSIONS_RESOURCE, e); } + debugExtensions(bucket); + } catch (IOException e) { + log.error("Failed to read components from " + EXTENSIONS_RESOURCE, e); } - - return result; + return bucket; } private void collectExtensions(InputStream inputStream, Set bucket) throws IOException { @@ -86,20 +72,15 @@ public class SpringComponentsFinder extends AbstractExtensionFinder { } } - protected boolean containsComponentsStorage(String pluginId) { - Assert.notNull(pluginId, "The pluginId cannot be null"); - long stamp = entryStampedLock.tryOptimisticRead(); - boolean contains = super.entries != null && super.entries.containsKey(pluginId); - if (!entryStampedLock.validate(stamp)) { - stamp = entryStampedLock.readLock(); - try { - return super.entries != null && entries.containsKey(pluginId); - } finally { - entryStampedLock.unlockRead(stamp); - } + @Override + public void pluginStateChanged(PluginStateEvent event) { + var pluginState = event.getPluginState(); + String pluginId = event.getPlugin().getPluginId(); + if (pluginState == PluginState.UNLOADED) { + entries.remove(pluginId); + } else if (pluginState == PluginState.CREATED || pluginState == PluginState.RESOLVED) { + entries.computeIfAbsent(pluginId, id -> readPluginStorage(event.getPlugin())); } - return contains; } } - diff --git a/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java b/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java index a0d546333..11c86ada4 100644 --- a/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java +++ b/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java @@ -1,18 +1,25 @@ package run.halo.app.plugin; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -import java.io.File; import java.io.FileNotFoundException; -import org.junit.jupiter.api.BeforeEach; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Set; 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.pf4j.PluginClassLoader; +import org.pf4j.PluginManager; +import org.pf4j.PluginState; +import org.pf4j.PluginStateEvent; import org.pf4j.PluginWrapper; import org.springframework.util.ResourceUtils; @@ -25,29 +32,64 @@ import org.springframework.util.ResourceUtils; @ExtendWith(MockitoExtension.class) class SpringComponentsFinderTest { - private File testFile; @Mock - private HaloPluginManager pluginManager; - @Mock - private PluginWrapper pluginWrapper; - @Mock - private PluginClassLoader pluginClassLoader; + private PluginManager pluginManager; @InjectMocks - private SpringComponentsFinder springComponentsFinder; + private SpringComponentsFinder finder; - @BeforeEach - void setUp() throws FileNotFoundException { - testFile = ResourceUtils.getFile("classpath:plugin/test-plugin-components.idx"); + @Test + void shouldNotInvokeReadClasspathStorages() { + assertThrows(UnsupportedOperationException.class, + () -> finder.readClasspathStorages() + ); } @Test - void containsPlugin() { - boolean exist = springComponentsFinder.containsComponentsStorage("NotExist"); - assertThat(exist).isFalse(); - - assertThatThrownBy(() -> springComponentsFinder.containsComponentsStorage(null)) - .hasMessage("The pluginId cannot be null"); + void shouldNotInvokeReadPluginsStorages() { + assertThrows(UnsupportedOperationException.class, + () -> finder.readPluginsStorages() + ); } + @Test + void shouldPutEntryIfPluginCreated() throws FileNotFoundException { + var pluginWrapper = mockPluginWrapper(); + when(pluginWrapper.getPluginState()).thenReturn(PluginState.CREATED); + + var event = new PluginStateEvent(pluginManager, pluginWrapper, null); + finder.pluginStateChanged(event); + + var classNames = finder.findClassNames("fake-plugin"); + assertEquals(Set.of("run.halo.fake.FakePlugin"), classNames); + } + + @Test + void shouldRemoveEntryIfPluginUnloaded() throws FileNotFoundException { + var pluginWrapper = mockPluginWrapper(); + when(pluginWrapper.getPluginState()).thenReturn(PluginState.CREATED); + + var event = new PluginStateEvent(pluginManager, pluginWrapper, null); + finder.pluginStateChanged(event); + + var classNames = finder.findClassNames("fake-plugin"); + assertFalse(classNames.isEmpty()); + + when(pluginWrapper.getPluginState()).thenReturn(PluginState.UNLOADED); + event = new PluginStateEvent(pluginManager, pluginWrapper, null); + finder.pluginStateChanged(event); + + classNames = finder.findClassNames("fake-plugin"); + assertTrue(classNames.isEmpty()); + } + + private PluginWrapper mockPluginWrapper() throws FileNotFoundException { + var pluginWrapper = mock(PluginWrapper.class); + when(pluginWrapper.getPluginId()).thenReturn("fake-plugin"); + + var pluginRootUrl = ResourceUtils.getURL("classpath:plugin/plugin-for-finder/"); + var classLoader = new URLClassLoader(new URL[] {pluginRootUrl}); + when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader); + return pluginWrapper; + } } \ No newline at end of file diff --git a/application/src/test/resources/plugin/plugin-for-finder/META-INF/plugin-components.idx b/application/src/test/resources/plugin/plugin-for-finder/META-INF/plugin-components.idx new file mode 100644 index 000000000..a40f507e4 --- /dev/null +++ b/application/src/test/resources/plugin/plugin-for-finder/META-INF/plugin-components.idx @@ -0,0 +1,2 @@ +# Generated by Halo +run.halo.fake.FakePlugin diff --git a/application/src/test/resources/plugin/test-plugin-components.idx b/application/src/test/resources/plugin/test-plugin-components.idx deleted file mode 100644 index 1835b4bbf..000000000 --- a/application/src/test/resources/plugin/test-plugin-components.idx +++ /dev/null @@ -1,2 +0,0 @@ -# Generated by Halo -run.halo.links.LinkPlugin