diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java index a9ae3b07f6..b669cb63fa 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java @@ -70,7 +70,6 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea private String author; private String category; private String avatarUrl; - private PluginCondition condition; @Override public PluginInformation clone() { @@ -82,9 +81,6 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea clone.setAuthor(author); clone.setCategory(category); clone.setAvatarUrl(avatarUrl); - if (condition != null) { - clone.setCondition(condition.clone()); - } return clone; } 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 4e91f5123b..e465c8306e 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -35,11 +35,15 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; //~--- JDK imports ------------------------------------------------------------ +import javax.inject.Inject; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; /** * @@ -48,24 +52,48 @@ import java.util.Optional; @Singleton public class DefaultPluginManager implements PluginManager { + private final PluginLoader loader; + private final PluginCenter center; + + @Inject + public DefaultPluginManager(PluginLoader loader, PluginCenter center) { + this.loader = loader; + this.center = center; + } + @Override public Optional getAvailable(String name) { - return Optional.empty(); + return center.getAvailable() + .stream() + .filter(filterByName(name)) + .filter(this::isNotInstalled) + .findFirst(); } @Override public Optional getInstalled(String name) { - return Optional.empty(); + return loader.getInstalledPlugins() + .stream() + .filter(filterByName(name)) + .findFirst(); } @Override public List getInstalled() { - return null; + return ImmutableList.copyOf(loader.getInstalledPlugins()); } @Override public List getAvailable() { - return null; + return center.getAvailable().stream().filter(this::isNotInstalled).collect(Collectors.toList()); + } + + private Predicate filterByName(String name) { + return (plugin) -> name.equals(plugin.getDescriptor().getInformation().getName()); + } + + private boolean isNotInstalled(AvailablePlugin availablePlugin) { + return !getInstalled(availablePlugin.getDescriptor().getInformation().getName()).isPresent(); } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java new file mode 100644 index 0000000000..a3817eba0f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java @@ -0,0 +1,55 @@ +package sonia.scm.plugin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.cache.Cache; +import sonia.scm.cache.CacheManager; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; +import sonia.scm.util.SystemUtil; + +import javax.inject.Inject; +import java.util.Set; + +public class PluginCenter { + + private static final String CACHE_NAME = "sonia.cache.plugins"; + + private static final Logger LOG = LoggerFactory.getLogger(PluginCenter.class); + + private final SCMContextProvider context; + private final ScmConfiguration configuration; + private final PluginCenterLoader loader; + private final Cache> cache; + + @Inject + public PluginCenter(SCMContextProvider context, CacheManager cacheManager, ScmConfiguration configuration, PluginCenterLoader loader) { + this.context = context; + this.configuration = configuration; + this.loader = loader; + this.cache = cacheManager.getCache(CACHE_NAME); + } + + synchronized Set getAvailable() { + String url = buildPluginUrl(configuration.getPluginUrl()); + Set plugins = cache.get(url); + if (plugins == null) { + LOG.debug("no cached available plugins found, start fetching"); + plugins = loader.load(url); + cache.put(url, plugins); + } else { + LOG.debug("return available plugins from cache"); + } + return plugins; + } + + private String buildPluginUrl(String url) { + String os = HttpUtil.encode(SystemUtil.getOS()); + String arch = SystemUtil.getArch(); + return url.replace("{version}", context.getVersion()) + .replace("{os}", os) + .replace("{arch}", arch); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java index ea445b3ede..972afa3099 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java @@ -1,26 +1,26 @@ package sonia.scm.plugin; import org.mapstruct.Mapper; -import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; import java.util.HashSet; -import java.util.List; import java.util.Set; @Mapper -public interface PluginCenterDtoMapper { +public abstract class PluginCenterDtoMapper { - @Mapping(source = "conditions", target = "condition") - PluginInformation map(PluginCenterDto.Plugin plugin); + static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class); - PluginCondition map(PluginCenterDto.Condition condition); + abstract PluginInformation map(PluginCenterDto.Plugin plugin); + abstract PluginCondition map(PluginCenterDto.Condition condition); - static Set map(List dtos) { - PluginCenterDtoMapper mapper = Mappers.getMapper(PluginCenterDtoMapper.class); - Set plugins = new HashSet<>(); - for (PluginCenterDto.Plugin plugin : dtos) { - plugins.add(mapper.map(plugin)); + Set map(PluginCenterDto pluginCenterDto) { + Set plugins = new HashSet<>(); + for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) { + AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( + map(plugin), map(plugin.getConditions()), plugin.getDependencies() + ); + plugins.add(new AvailablePlugin(descriptor)); } return plugins; } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java new file mode 100644 index 0000000000..2b0928891e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java @@ -0,0 +1,42 @@ +package sonia.scm.plugin; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.net.ahc.AdvancedHttpClient; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +class PluginCenterLoader { + + private static final Logger LOG = LoggerFactory.getLogger(PluginCenterLoader.class); + + private final AdvancedHttpClient client; + private final PluginCenterDtoMapper mapper; + + @Inject + public PluginCenterLoader(AdvancedHttpClient client) { + this(client, PluginCenterDtoMapper.INSTANCE); + } + + @VisibleForTesting + PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper) { + this.client = client; + this.mapper = mapper; + } + + Set load(String url) { + try { + LOG.info("fetch plugins from {}", url); + PluginCenterDto pluginCenterDto = client.get(url).request().contentFromJson(PluginCenterDto.class); + return mapper.map(pluginCenterDto); + } catch (IOException ex) { + LOG.error("failed to load plugins from plugin center, returning empty list"); + return Collections.emptySet(); + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java new file mode 100644 index 0000000000..22fe370915 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -0,0 +1,149 @@ +package sonia.scm.plugin; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.checkerframework.checker.nullness.Opt; +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 java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultPluginManagerTest { + + @Mock + private PluginLoader loader; + + @Mock + private PluginCenter center; + + @InjectMocks + private DefaultPluginManager manager; + + @Test + void shouldReturnInstalledPlugins() { + InstalledPlugin review = createInstalled("scm-review-plugin"); + InstalledPlugin git = createInstalled("scm-git-plugin"); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git)); + + List installed = manager.getInstalled(); + assertThat(installed).containsOnly(review, git); + } + + @Test + void shouldReturnReviewPlugin() { + InstalledPlugin review = createInstalled("scm-review-plugin"); + InstalledPlugin git = createInstalled("scm-git-plugin"); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git)); + + Optional plugin = manager.getInstalled("scm-review-plugin"); + assertThat(plugin).contains(review); + } + + @Test + void shouldReturnEmptyForNonInstalledPlugin() { + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of()); + + Optional plugin = manager.getInstalled("scm-review-plugin"); + assertThat(plugin).isEmpty(); + } + + @Test + void shouldReturnAvailablePlugins() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin"); + + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); + + List available = manager.getAvailable(); + assertThat(available).containsOnly(review, git); + } + + @Test + void shouldFilterOutAllInstalled() { + InstalledPlugin installedGit = createInstalled("scm-git-plugin"); + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); + + AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); + + List available = manager.getAvailable(); + assertThat(available).containsOnly(review); + } + + @Test + void shouldReturnAvailable() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); + + Optional available = manager.getAvailable("scm-git-plugin"); + assertThat(available).contains(git); + } + + @Test + void shouldReturnEmptyForNonExistingAvailable() { + AvailablePlugin review = createAvailable("scm-review-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); + + Optional available = manager.getAvailable("scm-git-plugin"); + assertThat(available).isEmpty(); + } + + @Test + void shouldReturnEmptyForInstalledPlugin() { + InstalledPlugin installedGit = createInstalled("scm-git-plugin"); + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); + + AvailablePlugin git = createAvailable("scm-git-plugin"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); + + Optional available = manager.getAvailable("scm-git-plugin"); + assertThat(available).isEmpty(); + } + + private AvailablePlugin createAvailable(String name) { + PluginInformation information = new PluginInformation(); + information.setName(name); + return createAvailable(information); + } + + private InstalledPlugin createInstalled(String name) { + PluginInformation information = new PluginInformation(); + information.setName(name); + return createInstalled(information); + } + + private InstalledPlugin createInstalled(PluginInformation information) { + InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS); + returnInformation(plugin, information); + return plugin; + } + + private AvailablePlugin createAvailable(PluginInformation information) { + AvailablePlugin plugin = mock(AvailablePlugin.class, Answers.RETURNS_DEEP_STUBS); + returnInformation(plugin, information); + return plugin; + } + + private void returnInformation(Plugin mockedPlugin, PluginInformation information) { + when(mockedPlugin.getDescriptor().getInformation()).thenReturn(information); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java index 831e847843..0c0b80ccfa 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java @@ -2,6 +2,11 @@ package sonia.scm.plugin; import com.google.common.collect.ImmutableSet; 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 java.util.ArrayList; import java.util.Arrays; @@ -11,11 +16,19 @@ import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; import static sonia.scm.plugin.PluginCenterDto.Plugin; import static sonia.scm.plugin.PluginCenterDto.*; +@ExtendWith(MockitoExtension.class) class PluginCenterDtoMapperTest { + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private PluginCenterDto dto; + + @InjectMocks + private PluginCenterDtoMapperImpl mapper; + @Test void shouldMapSinglePlugin() { Plugin plugin = new Plugin( @@ -31,16 +44,19 @@ class PluginCenterDtoMapperTest { ImmutableSet.of("scm-review-plugin"), new HashMap<>()); - PluginInformation result = PluginCenterDtoMapper.map(Collections.singletonList(plugin)).iterator().next(); + when(dto.getEmbedded().getPlugins()).thenReturn(Collections.singletonList(plugin)); + AvailablePluginDescriptor descriptor = mapper.map(dto).iterator().next().getDescriptor(); + PluginInformation information = descriptor.getInformation(); + PluginCondition condition = descriptor.getCondition(); - assertThat(result.getAuthor()).isEqualTo(plugin.getAuthor()); - assertThat(result.getCategory()).isEqualTo(plugin.getCategory()); - assertThat(result.getVersion()).isEqualTo(plugin.getVersion()); - assertThat(result.getCondition().getArch()).isEqualTo(plugin.getConditions().getArch()); - assertThat(result.getCondition().getMinVersion()).isEqualTo(plugin.getConditions().getMinVersion()); - assertThat(result.getCondition().getOs().iterator().next()).isEqualTo(plugin.getConditions().getOs().iterator().next()); - assertThat(result.getDescription()).isEqualTo(plugin.getDescription()); - assertThat(result.getName()).isEqualTo(plugin.getName()); + assertThat(information.getAuthor()).isEqualTo(plugin.getAuthor()); + assertThat(information.getCategory()).isEqualTo(plugin.getCategory()); + assertThat(information.getVersion()).isEqualTo(plugin.getVersion()); + assertThat(condition.getArch()).isEqualTo(plugin.getConditions().getArch()); + assertThat(condition.getMinVersion()).isEqualTo(plugin.getConditions().getMinVersion()); + assertThat(condition.getOs().iterator().next()).isEqualTo(plugin.getConditions().getOs().iterator().next()); + assertThat(information.getDescription()).isEqualTo(plugin.getDescription()); + assertThat(information.getName()).isEqualTo(plugin.getName()); } @Test @@ -71,12 +87,14 @@ class PluginCenterDtoMapperTest { ImmutableSet.of("scm-review-plugin"), new HashMap<>()); - Set resultSet = PluginCenterDtoMapper.map(Arrays.asList(plugin1, plugin2)); + when(dto.getEmbedded().getPlugins()).thenReturn(Arrays.asList(plugin1, plugin2)); - List pluginsList = new ArrayList<>(resultSet); + Set resultSet = mapper.map(dto); - PluginInformation pluginInformation1 = pluginsList.get(1); - PluginInformation pluginInformation2 = pluginsList.get(0); + List pluginsList = new ArrayList<>(resultSet); + + PluginInformation pluginInformation1 = pluginsList.get(1).getDescriptor().getInformation(); + PluginInformation pluginInformation2 = pluginsList.get(0).getDescriptor().getInformation(); assertThat(pluginInformation1.getAuthor()).isEqualTo(plugin1.getAuthor()); assertThat(pluginInformation1.getVersion()).isEqualTo(plugin1.getVersion()); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java new file mode 100644 index 0000000000..e3ebf995bd --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java @@ -0,0 +1,50 @@ +package sonia.scm.plugin; + +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.net.ahc.AdvancedHttpClient; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginCenterLoaderTest { + + private static final String PLUGIN_URL = "https://plugins.hitchhiker.com"; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private AdvancedHttpClient client; + + @Mock + private PluginCenterDtoMapper mapper; + + @InjectMocks + private PluginCenterLoader loader; + + @Test + void shouldFetch() throws IOException { + Set plugins = Collections.emptySet(); + PluginCenterDto dto = new PluginCenterDto(); + when(client.get(PLUGIN_URL).request().contentFromJson(PluginCenterDto.class)).thenReturn(dto); + when(mapper.map(dto)).thenReturn(plugins); + + Set fetched = loader.load(PLUGIN_URL); + assertThat(fetched).isSameAs(plugins); + } + + @Test + void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException { + when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch")); + + Set fetch = loader.load(PLUGIN_URL); + assertThat(fetch).isEmpty(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java new file mode 100644 index 0000000000..a76b4cb551 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java @@ -0,0 +1,73 @@ +package sonia.scm.plugin; + +import com.google.common.collect.ImmutableSet; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.cache.CacheManager; +import sonia.scm.cache.MapCacheManager; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.util.SystemUtil; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginCenterTest { + + private static final String PLUGIN_URL_BASE = "https://plugins.hitchhiker.com/"; + private static final String PLUGIN_URL = PLUGIN_URL_BASE + "{version}"; + + @Mock + private PluginCenterLoader loader; + + @Mock + private SCMContextProvider contextProvider; + + private ScmConfiguration configuration; + + private CacheManager cacheManager; + + private PluginCenter pluginCenter; + + @BeforeEach + void setUpPluginCenter() { + when(contextProvider.getVersion()).thenReturn("2.0.0"); + + cacheManager = new MapCacheManager(); + + configuration = new ScmConfiguration(); + configuration.setPluginUrl(PLUGIN_URL); + + pluginCenter = new PluginCenter(contextProvider, cacheManager, configuration, loader); + } + + @Test + void shouldFetchPlugins() { + Set plugins = new HashSet<>(); + when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(plugins); + + assertThat(pluginCenter.getAvailable()).isSameAs(plugins); + } + + @Test + void shouldCache() { + Set first = new HashSet<>(); + when(loader.load(anyString())).thenReturn(first, new HashSet<>()); + + assertThat(pluginCenter.getAvailable()).isSameAs(first); + assertThat(pluginCenter.getAvailable()).isSameAs(first); + } + +}