diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae68625bb..c4329cb418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- Fire various plugin events ([#1088](https://github.com/scm-manager/scm-manager/pull/1088)) - Display version for plugins ([#1089](https://github.com/scm-manager/scm-manager/pull/1089) ### Changed diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginCenterErrorEvent.java b/scm-core/src/main/java/sonia/scm/plugin/PluginCenterErrorEvent.java new file mode 100644 index 0000000000..b83a06205d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginCenterErrorEvent.java @@ -0,0 +1,30 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import sonia.scm.event.Event; + +@Event +public class PluginCenterErrorEvent {} diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginEvent.java b/scm-core/src/main/java/sonia/scm/plugin/PluginEvent.java new file mode 100644 index 0000000000..7802b3954d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginEvent.java @@ -0,0 +1,43 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import lombok.Getter; +import sonia.scm.event.Event; + +@Getter +@Event +public class PluginEvent { + private final PluginEventType eventType; + private final AvailablePlugin plugin; + public PluginEvent(PluginEventType eventType, AvailablePlugin plugin) { + this.eventType = eventType; + this.plugin = plugin; + } + + public enum PluginEventType { + INSTALLED, INSTALLATION_FAILED + } +} diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index f7ae938b5b..a95599aa08 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -24,7 +24,8 @@ SOFTWARE. --> - + 4.0.0 @@ -462,7 +463,6 @@ 2.1.1 provided - diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index ba41ada287..bb6dc1491f 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; import com.google.common.annotations.VisibleForTesting; @@ -31,7 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.NotFoundException; import sonia.scm.event.ScmEventBus; -import sonia.scm.lifecycle.RestartEvent; import sonia.scm.lifecycle.Restarter; import sonia.scm.version.Version; @@ -52,7 +51,6 @@ import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ @Singleton @@ -64,17 +62,19 @@ public class DefaultPluginManager implements PluginManager { private final PluginCenter center; private final PluginInstaller installer; private final Restarter restarter; + private final ScmEventBus eventBus; private final Collection pendingInstallQueue = new ArrayList<>(); private final Collection pendingUninstallQueue = new ArrayList<>(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); @Inject - public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter) { + public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) { this.loader = loader; this.center = center; this.installer = installer; this.restarter = restarter; + this.eventBus = eventBus; this.computeInstallationDependencies(); } @@ -172,8 +172,10 @@ public class DefaultPluginManager implements PluginManager { PendingPluginInstallation pending = installer.install(plugin); dependencyTracker.addInstalled(plugin.getDescriptor()); pendingInstallations.add(pending); + eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLED, plugin)); } catch (PluginInstallException ex) { cancelPending(pendingInstallations); + eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLATION_FAILED, plugin)); throw ex; } } @@ -255,7 +257,7 @@ public class DefaultPluginManager implements PluginManager { Set dependencies = plugin.getDescriptor().getDependencies(); if (dependencies != null) { - for (String dependency: dependencies){ + for (String dependency : dependencies) { collectPluginsToInstall(plugins, dependency, false); } } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java index efc7d9f136..efc967df49 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java @@ -27,10 +27,10 @@ package sonia.scm.plugin; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.event.ScmEventBus; import sonia.scm.net.ahc.AdvancedHttpClient; import javax.inject.Inject; -import java.io.IOException; import java.util.Collections; import java.util.Set; @@ -40,16 +40,18 @@ class PluginCenterLoader { private final AdvancedHttpClient client; private final PluginCenterDtoMapper mapper; + private final ScmEventBus eventBus; @Inject - public PluginCenterLoader(AdvancedHttpClient client) { - this(client, PluginCenterDtoMapper.INSTANCE); + public PluginCenterLoader(AdvancedHttpClient client, ScmEventBus eventBus) { + this(client, PluginCenterDtoMapper.INSTANCE, eventBus); } @VisibleForTesting - PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper) { + PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper, ScmEventBus eventBus) { this.client = client; this.mapper = mapper; + this.eventBus = eventBus; } Set load(String url) { @@ -59,8 +61,8 @@ class PluginCenterLoader { return mapper.map(pluginCenterDto); } catch (Exception ex) { LOG.error("failed to load plugins from plugin center, returning empty list", ex); + eventBus.post(new PluginCenterErrorEvent()); return Collections.emptySet(); } } - } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java index 51a7001140..8301e6fb76 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java @@ -31,22 +31,18 @@ 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.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.lifecycle.Restarter; import sonia.scm.plugin.AvailablePlugin; -import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.PluginInformation; import java.net.URI; -import java.util.Collections; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static sonia.scm.plugin.PluginTestHelper.createAvailable; import static sonia.scm.plugin.PluginTestHelper.createInstalled; diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 8f3f7bbe57..03b12f92cf 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -36,16 +36,19 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; import sonia.scm.ScmConstraintViolationException; +import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.Restarter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -53,6 +56,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.in; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doNothing; @@ -83,6 +87,12 @@ class DefaultPluginManagerTest { @Mock private Restarter restarter; + @Mock + private ScmEventBus eventBus; + + @Captor + private ArgumentCaptor eventCaptor; + @InjectMocks private DefaultPluginManager manager; @@ -537,8 +547,36 @@ class DefaultPluginManagerTest { verify(installer, never()).install(oldScriptPlugin); } + + @Test + void shouldFirePluginEventOnInstallation() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + + manager.install("scm-review-plugin", false); + + verify(eventBus).post(eventCaptor.capture()); + + assertThat(eventCaptor.getValue().getEventType()).isEqualTo(PluginEvent.PluginEventType.INSTALLED); + assertThat(eventCaptor.getValue().getPlugin()).isEqualTo(review); + } + + @Test + void shouldFirePluginEventOnFailedInstallation() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(review); + + assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false)); + + verify(eventBus).post(eventCaptor.capture()); + assertThat(eventCaptor.getValue().getEventType()).isEqualTo(PluginEvent.PluginEventType.INSTALLATION_FAILED); + assertThat(eventCaptor.getValue().getPlugin()).isEqualTo(review); + } + } + @Nested class WithoutReadPermissions { diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java index 01fe540206..cc59e42f62 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; import org.junit.jupiter.api.Test; @@ -30,6 +30,7 @@ import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.event.ScmEventBus; import sonia.scm.net.ahc.AdvancedHttpClient; import java.io.IOException; @@ -37,6 +38,8 @@ import java.util.Collections; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -50,6 +53,9 @@ class PluginCenterLoaderTest { @Mock private PluginCenterDtoMapper mapper; + @Mock + private ScmEventBus eventBus; + @InjectMocks private PluginCenterLoader loader; @@ -71,4 +77,13 @@ class PluginCenterLoaderTest { Set fetch = loader.load(PLUGIN_URL); assertThat(fetch).isEmpty(); } + + @Test + void shouldFirePluginCenterErrorEvent() throws IOException { + when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch")); + + loader.load(PLUGIN_URL); + + verify(eventBus).post(any(PluginCenterErrorEvent.class)); + } }