From d189558e9c7b3994254eaa8d67dff0169c21d5dd Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Wed, 26 Aug 2020 18:30:15 -0500 Subject: [PATCH] SONAR-13643 Save plugins with type --- .../LoadReportAnalysisMetadataHolderStep.java | 8 +- ...dReportAnalysisMetadataHolderStepTest.java | 5 + .../ce/container/CePluginJarExploder.java | 2 +- .../ce/container/CePluginRepository.java | 24 +- .../container/ComputeEngineContainerImpl.java | 4 +- .../ce/container/CePluginRepositoryTest.java | 69 ++- .../java/org/sonar/db/plugin/PluginDto.java | 28 +- .../org/sonar/db/plugin/PluginMapper.xml | 4 + server/sonar-db-dao/src/schema/schema-sq.ddl | 3 +- .../org/sonar/db/plugin/PluginDaoTest.java | 63 ++- .../version/v85/AddTypeToPlugins.java | 44 ++ .../v85/AlterTypeInPluginNotNullable.java | 44 ++ .../db/migration/version/v85/DbVersion85.java | 3 + .../version/v85/PopulateTypeInPlugins.java | 37 ++ .../version/v85/AddTypeToPluginsTest.java | 57 +++ .../v85/AlterTypeInPluginNotNullableTest.java | 58 +++ .../v85/PopulateTypeInPluginsTest.java | 62 +++ .../v85/AddTypeToPluginsTest/schema.sql | 10 + .../schema.sql | 11 + .../v85/PopulateTypeInPluginsTest/schema.sql | 11 + .../plugins/ServerExtensionInstallerTest.java | 5 + ...nFileSystem.java => PluginCompressor.java} | 51 +-- .../server/plugins/PluginDownloader.java | 11 +- ...lledPlugin.java => PluginFilesAndMd5.java} | 13 +- .../sonar/server/plugins/PluginJarLoader.java | 251 ++++++++++ .../org/sonar/server/plugins/PluginType.java | 24 + .../server/plugins/PluginUninstaller.java | 99 +++- .../sonar/server/plugins/ServerPlugin.java | 73 +++ .../server/plugins/ServerPluginInfo.java | 79 ++++ .../plugins/ServerPluginJarExploder.java | 24 +- .../server/plugins/ServerPluginManager.java | 99 ++++ .../plugins/ServerPluginRepository.java | 372 ++------------- .../server/plugins/UpdateCenterClient.java | 6 +- .../plugins/UpdateCenterMatrixFactory.java | 4 +- ...temTest.java => PluginCompressorTest.java} | 59 +-- .../server/plugins/PluginDownloaderTest.java | 4 +- .../server/plugins/PluginFilesAndMd5Test.java | 62 +++ .../server/plugins/PluginJarLoaderTest.java | 319 +++++++++++++ .../server/plugins/PluginUninstallerTest.java | 127 ++++-- .../server/plugins/ServerPluginInfoTest.java | 49 ++ .../plugins/ServerPluginJarExploderTest.java | 5 +- .../plugins/ServerPluginManagerTest.java | 104 +++++ .../plugins/ServerPluginRepositoryTest.java | 428 ++---------------- .../plugins/UpdateCenterClientTest.java | 5 +- .../UpdateCenterMatrixFactoryTest.java | 8 +- .../server/startup/GeneratePluginIndex.java | 23 +- .../sonar/server/startup/RegisterPlugins.java | 32 +- .../startup/GeneratePluginIndexTest.java | 27 +- .../server/startup/RegisterPluginsTest.java | 78 +++- .../server/platform/ws/UpgradesAction.java | 2 +- .../server/plugins/ws/AvailableAction.java | 9 +- .../server/plugins/ws/DownloadAction.java | 20 +- .../server/plugins/ws/InstallAction.java | 2 +- .../server/plugins/ws/InstalledAction.java | 43 +- .../server/plugins/ws/PendingAction.java | 40 +- .../server/plugins/ws/PluginWSCommons.java | 14 +- .../server/plugins/ws/UninstallAction.java | 22 +- .../sonar/server/plugins/ws/UpdateAction.java | 26 +- .../server/plugins/ws/UpdatesAction.java | 2 +- .../language/LanguageValidationTest.java | 10 +- .../platform/ws/UpgradesActionTest.java | 4 +- ...tUpdateCenterBasedPluginsWsActionTest.java | 2 +- .../plugins/ws/AvailableActionTest.java | 4 +- .../server/plugins/ws/DownloadActionTest.java | 70 +-- .../server/plugins/ws/InstallActionTest.java | 5 +- .../plugins/ws/InstalledActionTest.java | 141 ++++-- .../server/plugins/ws/PendingActionTest.java | 7 +- .../plugins/ws/PluginWSCommonsTest.java | 36 +- .../plugins/ws/UninstallActionTest.java | 76 +--- .../server/plugins/ws/UpdateActionTest.java | 4 +- ...java => PluginsActionTestFilesAndMD5.java} | 2 +- .../platformlevel/PlatformLevel2.java | 13 +- .../platformlevel/PlatformLevel4.java | 2 +- .../sonar/core/platform/ExplodedPlugin.java | 9 +- ...uginLoader.java => PluginClassLoader.java} | 46 +- .../org/sonar/core/platform/PluginInfo.java | 164 +++---- .../core/platform/PluginJarExploder.java | 6 +- .../sonar/core/platform/PluginRepository.java | 4 +- ...erTest.java => PluginClassLoaderTest.java} | 44 +- .../sonar/core/platform/PluginInfoTest.java | 31 +- .../core/platform/PluginJarExploderTest.java | 4 +- .../scanner/bootstrap/GlobalContainer.java | 4 +- .../bootstrap/ScannerPluginJarExploder.java | 2 +- .../bootstrap/ScannerPluginRepository.java | 25 +- .../ScannerPluginRepositoryTest.java | 21 +- 85 files changed, 2411 insertions(+), 1493 deletions(-) create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPlugins.java create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullable.java create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPlugins.java create mode 100644 server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest.java create mode 100644 server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest.java create mode 100644 server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest.java create mode 100644 server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest/schema.sql create mode 100644 server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest/schema.sql create mode 100644 server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest/schema.sql rename server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/{PluginFileSystem.java => PluginCompressor.java} (68%) rename server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/{InstalledPlugin.java => PluginFilesAndMd5.java} (84%) create mode 100644 server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java create mode 100644 server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginType.java create mode 100644 server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPlugin.java create mode 100644 server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginInfo.java create mode 100644 server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginManager.java rename server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/{PluginFileSystemTest.java => PluginCompressorTest.java} (73%) create mode 100644 server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFilesAndMd5Test.java create mode 100644 server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginJarLoaderTest.java create mode 100644 server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginInfoTest.java create mode 100644 server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginManagerTest.java rename server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/{InstalledPluginsActionTest.java => PluginsActionTestFilesAndMD5.java} (98%) rename sonar-core/src/main/java/org/sonar/core/platform/{PluginLoader.java => PluginClassLoader.java} (82%) rename sonar-core/src/test/java/org/sonar/core/platform/{PluginLoaderTest.java => PluginClassLoaderTest.java} (80%) diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStep.java index 9fe44ff2796..582475e04da 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStep.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStep.java @@ -147,11 +147,11 @@ public class LoadReportAnalysisMetadataHolderStep implements ComputationStep { private void loadQualityProfiles(ScannerReport.Metadata reportMetadata, Organization organization) { checkQualityProfilesConsistency(reportMetadata, organization); - analysisMetadata.setQProfilesByLanguage(reportMetadata.getQprofilesPerLanguage().values().stream() + analysisMetadata.setQProfilesByLanguage(reportMetadata.getQprofilesPerLanguageMap().values().stream() .collect(toMap( QProfile::getLanguage, qp -> new QualityProfile(qp.getKey(), qp.getName(), qp.getLanguage(), new Date(qp.getRulesUpdatedAt()))))); - analysisMetadata.setScannerPluginsByKey(reportMetadata.getPluginsByKey().values().stream() + analysisMetadata.setScannerPluginsByKey(reportMetadata.getPluginsByKeyMap().values().stream() .collect(toMap( Plugin::getKey, p -> new ScannerPlugin(p.getKey(), getBasePluginKey(p), p.getUpdatedAt())))); @@ -171,9 +171,9 @@ public class LoadReportAnalysisMetadataHolderStep implements ComputationStep { * Check that the Quality profiles sent by scanner correctly relate to the project organization. */ private void checkQualityProfilesConsistency(ScannerReport.Metadata metadata, Organization organization) { - List profileKeys = metadata.getQprofilesPerLanguage().values().stream() + List profileKeys = metadata.getQprofilesPerLanguageMap().values().stream() .map(QProfile::getKey) - .collect(toList(metadata.getQprofilesPerLanguage().size())); + .collect(toList(metadata.getQprofilesPerLanguageMap().size())); try (DbSession dbSession = dbClient.openSession(false)) { List profiles = dbClient.qualityProfileDao().selectByUuids(dbSession, profileKeys); String badKeys = profiles.stream() diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStepTest.java index 17d681d58e9..4d10423cfff 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStepTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStepTest.java @@ -486,6 +486,11 @@ public class LoadReportAnalysisMetadataHolderStepTest { throw new UnsupportedOperationException(); } + @Override + public Collection getPluginInstances() { + throw new UnsupportedOperationException(); + } + @Override public boolean hasPlugin(String key) { return pluginsMap.containsKey(key); diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginJarExploder.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginJarExploder.java index 18959e76b31..b5d95a6f04c 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginJarExploder.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginJarExploder.java @@ -51,7 +51,7 @@ public class CePluginJarExploder extends PluginJarExploder { File jarTarget = new File(toDir, jarSource.getName()); FileUtils.copyFile(jarSource, jarTarget); ZipUtils.unzip(jarSource, toDir, newLibFilter()); - return explodeFromUnzippedDir(pluginInfo.getKey(), jarTarget, toDir); + return explodeFromUnzippedDir(pluginInfo, jarTarget, toDir); } catch (Exception e) { throw new IllegalStateException(String.format( "Fail to unzip plugin [%s] %s to %s", pluginInfo.getKey(), pluginInfo.getNonNullJarFile().getAbsolutePath(), toDir.getAbsolutePath()), e); diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java index 4c9476647ce..cc85465e78b 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java @@ -26,12 +26,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; import org.picocontainer.Startable; import org.sonar.api.Plugin; import org.sonar.api.utils.log.Loggers; +import org.sonar.core.platform.ExplodedPlugin; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.core.platform.PluginInfo; -import org.sonar.core.platform.PluginLoader; import org.sonar.core.platform.PluginRepository; import org.sonar.server.platform.ServerFileSystem; @@ -49,16 +51,18 @@ public class CePluginRepository implements PluginRepository, Startable { private static final String NOT_STARTED_YET = "not started yet"; private final ServerFileSystem fs; - private final PluginLoader loader; + private final PluginClassLoader loader; + private final CePluginJarExploder cePluginJarExploder; private final AtomicBoolean started = new AtomicBoolean(false); // following fields are available after startup private final Map pluginInfosByKeys = new HashMap<>(); private final Map pluginInstancesByKeys = new HashMap<>(); - public CePluginRepository(ServerFileSystem fs, PluginLoader loader) { + public CePluginRepository(ServerFileSystem fs, PluginClassLoader loader, CePluginJarExploder cePluginJarExploder) { this.fs = fs; this.loader = loader; + this.cePluginJarExploder = cePluginJarExploder; } @Override @@ -66,7 +70,8 @@ public class CePluginRepository implements PluginRepository, Startable { Loggers.get(getClass()).info("Load plugins"); registerPluginsFromDir(fs.getInstalledBundledPluginsDir()); registerPluginsFromDir(fs.getInstalledExternalPluginsDir()); - pluginInstancesByKeys.putAll(loader.load(pluginInfosByKeys)); + Map explodedPluginsByKey = extractPlugins(pluginInfosByKeys); + pluginInstancesByKeys.putAll(loader.load(explodedPluginsByKey)); started.set(true); } @@ -77,6 +82,12 @@ public class CePluginRepository implements PluginRepository, Startable { } } + private Map extractPlugins(Map pluginsByKey) { + return pluginsByKey.values().stream() + .map(cePluginJarExploder::explode) + .collect(Collectors.toMap(ExplodedPlugin::getKey, p -> p)); + } + @Override public void stop() { // close classloaders @@ -110,6 +121,11 @@ public class CePluginRepository implements PluginRepository, Startable { return plugin; } + @Override + public Collection getPluginInstances() { + return pluginInstancesByKeys.values(); + } + @Override public boolean hasPlugin(String key) { checkState(started.get(), NOT_STARTED_YET); diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index f7e3793a818..7d57bbfcb72 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -77,7 +77,7 @@ import org.sonar.core.platform.EditionProvider; import org.sonar.core.platform.Module; import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.core.platform.PluginClassloaderFactory; -import org.sonar.core.platform.PluginLoader; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.core.util.UuidFactoryImpl; import org.sonar.db.DBSessionsImpl; import org.sonar.db.DaoModule; @@ -335,7 +335,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { // plugins PluginClassloaderFactory.class, CePluginJarExploder.class, - PluginLoader.class, + PluginClassLoader.class, CePluginRepository.class, InstalledPluginReferentialFactory.class, ComputeEngineExtensionInstaller.class, diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/CePluginRepositoryTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/CePluginRepositoryTest.java index 047eebf6ead..4e3c93dd164 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/CePluginRepositoryTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/CePluginRepositoryTest.java @@ -20,21 +20,22 @@ package org.sonar.ce.container; import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.junit.After; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; import org.sonar.api.Plugin; -import org.sonar.core.platform.PluginInfo; -import org.sonar.core.platform.PluginLoader; +import org.sonar.core.platform.ExplodedPlugin; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.server.platform.ServerFileSystem; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -43,12 +44,10 @@ public class CePluginRepositoryTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); - @Rule - public ExpectedException expectedException = ExpectedException.none(); - private ServerFileSystem fs = mock(ServerFileSystem.class, Mockito.RETURNS_DEEP_STUBS); - private PluginLoader pluginLoader = new DumbPluginLoader(); - private CePluginRepository underTest = new CePluginRepository(fs, pluginLoader); + private PluginClassLoader pluginClassLoader = new DumbPluginClassLoader(); + private CePluginJarExploder cePluginJarExploder = new CePluginJarExploder(fs); + private CePluginRepository underTest = new CePluginRepository(fs, pluginClassLoader, cePluginJarExploder); @After public void tearDown() { @@ -67,8 +66,9 @@ public class CePluginRepositoryTest { } @Test - public void load_plugins() { + public void load_plugins() throws IOException { String pluginKey = "test"; + when(fs.getTempDir()).thenReturn(temp.newFolder()); when(fs.getInstalledExternalPluginsDir()).thenReturn(new File("src/test/plugins/sonar-test-plugin/target")); underTest.start(); @@ -76,74 +76,69 @@ public class CePluginRepositoryTest { assertThat(underTest.getPluginInfos()).extracting("key").containsOnly(pluginKey); assertThat(underTest.getPluginInfo(pluginKey).getKey()).isEqualTo(pluginKey); assertThat(underTest.getPluginInstance(pluginKey)).isNotNull(); + assertThat(underTest.getPluginInstances()).isNotEmpty(); assertThat(underTest.hasPlugin(pluginKey)).isTrue(); } @Test public void getPluginInfo_fails_if_plugin_does_not_exist() throws Exception { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Plugin [foo] does not exist"); - // empty folder when(fs.getInstalledExternalPluginsDir()).thenReturn(temp.newFolder()); underTest.start(); - underTest.getPluginInfo("foo"); + assertThatThrownBy(() -> underTest.getPluginInfo("foo")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Plugin [foo] does not exist"); } @Test public void getPluginInstance_fails_if_plugin_does_not_exist() throws Exception { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Plugin [foo] does not exist"); - // empty folder when(fs.getInstalledExternalPluginsDir()).thenReturn(temp.newFolder()); underTest.start(); - underTest.getPluginInstance("foo"); + assertThatThrownBy(() -> underTest.getPluginInstance("foo")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Plugin [foo] does not exist"); } @Test public void getPluginInstance_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.getPluginInstance("foo"); + assertThatThrownBy(() -> underTest.getPluginInstance("foo")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("not started yet"); } @Test public void getPluginInfo_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.getPluginInfo("foo"); + assertThatThrownBy(() -> underTest.getPluginInfo("foo")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("not started yet"); } @Test public void hasPlugin_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.hasPlugin("foo"); + assertThatThrownBy(() -> underTest.hasPlugin("foo")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("not started yet"); } @Test public void getPluginInfos_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.getPluginInfos(); + assertThatThrownBy(() -> underTest.getPluginInfos()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("not started yet"); } - private static class DumbPluginLoader extends PluginLoader { + private static class DumbPluginClassLoader extends PluginClassLoader { - public DumbPluginLoader() { - super(null, null); + public DumbPluginClassLoader() { + super(null); } /** * Does nothing except returning the specified list of plugins */ @Override - public Map load(Map infoByKeys) { + public Map load(Map infoByKeys) { Map result = new HashMap<>(); for (String pluginKey : infoByKeys.keySet()) { result.put(pluginKey, mock(Plugin.class)); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/plugin/PluginDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/plugin/PluginDto.java index 6b3169ab06f..375d4e847f7 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/plugin/PluginDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/plugin/PluginDto.java @@ -25,17 +25,19 @@ import org.apache.commons.lang.builder.ToStringBuilder; public class PluginDto { /** Technical unique identifier, can't be null */ - private String uuid; + private String uuid = null; /** Plugin key, unique, can't be null */ - private String kee; + private String kee = null; /** Base plugin key, can be null */ - private String basePluginKey; + private String basePluginKey = null; /** JAR file MD5 checksum, can't be null */ - private String fileHash; + private String fileHash = null; + // core feature or not + private Type type = null; /** Time plugin was first installed */ - private long createdAt; + private long createdAt = 0L; /** Time of last plugin update (=md5 change) */ - private long updatedAt; + private long updatedAt = 0L; public String getUuid() { return uuid; @@ -92,6 +94,20 @@ public class PluginDto { return this; } + public Type getType() { + return type; + } + + public PluginDto setType(Type type) { + this.type = type; + return this; + } + + public enum Type { + BUNDLED, + EXTERNAL + } + @Override public String toString() { return new ToStringBuilder(this) diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/plugin/PluginMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/plugin/PluginMapper.xml index 0ff5f6cf543..ce585f75d67 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/plugin/PluginMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/plugin/PluginMapper.xml @@ -9,6 +9,7 @@ kee, base_plugin_key as basePluginKey, file_hash as fileHash, + type, created_at as createdAt, updated_at as updatedAt @@ -31,6 +32,7 @@ kee, base_plugin_key, file_hash, + type, created_at, updated_at ) values ( @@ -38,6 +40,7 @@ #{kee,jdbcType=VARCHAR}, #{basePluginKey,jdbcType=VARCHAR}, #{fileHash,jdbcType=VARCHAR}, + #{type,jdbcType=VARCHAR}, #{createdAt,jdbcType=TIMESTAMP}, #{updatedAt,jdbcType=TIMESTAMP} ) @@ -47,6 +50,7 @@ update plugins set base_plugin_key=#{basePluginKey,jdbcType=VARCHAR}, file_hash=#{fileHash,jdbcType=VARCHAR}, + type=#{type,jdbcType=VARCHAR}, updated_at=#{updatedAt,jdbcType=BIGINT} where uuid=#{uuid,jdbcType=VARCHAR} diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index 9345fa56fba..10ff174c94d 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -602,7 +602,8 @@ CREATE TABLE "PLUGINS"( "BASE_PLUGIN_KEY" VARCHAR(200), "FILE_HASH" VARCHAR(200) NOT NULL, "CREATED_AT" BIGINT NOT NULL, - "UPDATED_AT" BIGINT NOT NULL + "UPDATED_AT" BIGINT NOT NULL, + "TYPE" VARCHAR(10) NOT NULL ); ALTER TABLE "PLUGINS" ADD CONSTRAINT "PK_PLUGINS" PRIMARY KEY("UUID"); CREATE UNIQUE INDEX "PLUGINS_KEY" ON "PLUGINS"("KEE"); diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/plugin/PluginDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/plugin/PluginDaoTest.java index 40492b676db..65d1abf3980 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/plugin/PluginDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/plugin/PluginDaoTest.java @@ -19,15 +19,17 @@ */ package org.sonar.db.plugin; -import java.util.Optional; import javax.annotation.Nullable; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.utils.System2; import org.sonar.db.DbTester; +import org.sonar.db.plugin.PluginDto.Type; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.plugin.PluginDto.Type.BUNDLED; +import static org.sonar.db.plugin.PluginDto.Type.EXTERNAL; public class PluginDaoTest { @@ -40,57 +42,38 @@ public class PluginDaoTest { @Test public void selectByKey() { - insertPlugin("a", "java", null, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1500000000000L, 1600000000000L); - insertPlugin("b", "javacustom", "java", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 1500000000000L, 1600000000000L); + insertPlugins(); assertThat(underTest.selectByKey(db.getSession(), "java2")).isEmpty(); - Optional plugin = underTest.selectByKey(db.getSession(), "java"); - assertThat(plugin.isPresent()).isTrue(); - assertThat(plugin.get().getUuid()).isEqualTo("a"); - assertThat(plugin.get().getKee()).isEqualTo("java"); - assertThat(plugin.get().getBasePluginKey()).isNull(); - assertThat(plugin.get().getFileHash()).isEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - assertThat(plugin.get().getCreatedAt()).isEqualTo(1500000000000L); - assertThat(plugin.get().getUpdatedAt()).isEqualTo(1600000000000L); + assertPlugin("java", "a", null, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", BUNDLED, 1500000000000L, 1600000000000L); } @Test public void selectAll() { - insertPlugin("a", "java", null, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1500000000000L, 1600000000000L); - insertPlugin("b", "javacustom", "java", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 1500000000000L, 1600000000000L); - + insertPlugins(); assertThat(underTest.selectAll(db.getSession())).hasSize(2); } @Test public void insert() { - insertPlugin("a", "java", null, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1500000000000L, 1600000000000L); - insertPlugin("b", "javacustom", "java", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 1500000000000L, 1600000000000L); + insertPlugins(); underTest.insert(db.getSession(), new PluginDto() .setUuid("c") .setKee("javascript") .setBasePluginKey("java") .setFileHash("cccccccccccccccccccccccccccccccc") + .setType(EXTERNAL) .setCreatedAt(1L) .setUpdatedAt(2L)); - Optional plugin = underTest.selectByKey(db.getSession(), "javascript"); - assertThat(plugin.isPresent()).isTrue(); - assertThat(plugin.get().getUuid()).isEqualTo("c"); - assertThat(plugin.get().getKee()).isEqualTo("javascript"); - assertThat(plugin.get().getBasePluginKey()).isEqualTo("java"); - assertThat(plugin.get().getFileHash()).isEqualTo("cccccccccccccccccccccccccccccccc"); - assertThat(plugin.get().getCreatedAt()).isEqualTo(1L); - assertThat(plugin.get().getUpdatedAt()).isEqualTo(2L); + assertPlugin("javascript", "c", "java", "cccccccccccccccccccccccccccccccc", EXTERNAL, 1L, 2L); } @Test public void update() { - insertPlugin("a", "java", null, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1500000000000L, 1600000000000L); - insertPlugin("b", "javacustom", "java", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 1500000000000L, 1600000000000L); - + insertPlugins(); PluginDto plugin = underTest.selectByKey(db.getSession(), "java").get(); plugin.setBasePluginKey("foo"); @@ -98,22 +81,32 @@ public class PluginDaoTest { plugin.setUpdatedAt(3L); underTest.update(db.getSession(), plugin); + assertPlugin("java", "a", "foo", "abc", BUNDLED, 1500000000000L, 3L); + } + + private void assertPlugin(String key, String uuid, @Nullable String basePluginKey, String fileHash, Type type, long cretedAt, long updatedAt) { + PluginDto plugin = underTest.selectByKey(db.getSession(), key).get(); + assertThat(plugin.getUuid()).isEqualTo(uuid); + assertThat(plugin.getKee()).isEqualTo(key); + assertThat(plugin.getBasePluginKey()).isEqualTo(basePluginKey); + assertThat(plugin.getFileHash()).isEqualTo(fileHash); + assertThat(plugin.getType()).isEqualTo(type); + assertThat(plugin.getCreatedAt()).isEqualTo(cretedAt); + assertThat(plugin.getUpdatedAt()).isEqualTo(updatedAt); + } - plugin = underTest.selectByKey(db.getSession(), "java").get(); - assertThat(plugin.getUuid()).isEqualTo("a"); - assertThat(plugin.getKee()).isEqualTo("java"); - assertThat(plugin.getBasePluginKey()).isEqualTo("foo"); - assertThat(plugin.getFileHash()).isEqualTo("abc"); - assertThat(plugin.getCreatedAt()).isEqualTo(1500000000000L); - assertThat(plugin.getUpdatedAt()).isEqualTo(3L); + private void insertPlugins() { + insertPlugin("a", "java", null, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", BUNDLED, 1500000000000L, 1600000000000L); + insertPlugin("b", "javacustom", "java", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", EXTERNAL, 1500000000000L, 1600000000000L); } - private void insertPlugin(String uuid, String key, @Nullable String basePluginKey, String fileHash, long createdAt, long updatedAt) { + private void insertPlugin(String uuid, String key, @Nullable String basePluginKey, String fileHash, Type type, long createdAt, long updatedAt) { db.executeInsert("PLUGINS", "uuid", uuid, "kee", key, "base_plugin_key", basePluginKey, "file_hash", fileHash, + "type", type.name(), "created_at", createdAt, "updated_at", updatedAt); db.commit(); diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPlugins.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPlugins.java new file mode 100644 index 00000000000..13c2f613e1c --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPlugins.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v85; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.def.VarcharColumnDef; +import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +public class AddTypeToPlugins extends DdlChange { + private static final String TABLE_NAME = "plugins"; + private static final String COLUMN_NAME = "type"; + + public AddTypeToPlugins(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME).addColumn(VarcharColumnDef.newVarcharColumnDefBuilder() + .setLimit(10) + .setIsNullable(true) + .setColumnName(COLUMN_NAME) + .build()).build()); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullable.java new file mode 100644 index 00000000000..c8e7634e36c --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullable.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v85; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.def.VarcharColumnDef; +import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +public class AlterTypeInPluginNotNullable extends DdlChange { + private static final String TABLE_NAME = "plugins"; + private static final String COLUMN_NAME = "type"; + + public AlterTypeInPluginNotNullable(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME).updateColumn(VarcharColumnDef.newVarcharColumnDefBuilder() + .setLimit(10) + .setIsNullable(false) + .setColumnName(COLUMN_NAME) + .build()).build()); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/DbVersion85.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/DbVersion85.java index cf4917d8abe..d6bb98a5bdb 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/DbVersion85.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/DbVersion85.java @@ -44,6 +44,9 @@ public class DbVersion85 implements DbVersion { .add(4013, "add index on 'issue_key' for table 'issue_changes'", AddIndexOnIssueKeyForIssueChangesTable.class) .add(4014, "add index on 'kee' for table 'issue_changes'", AddIndexOnKeeForIssueChangesTable.class) .add(4015, "add index on 'project_uuid' for table 'issue_changes'", AddIndexOnProjectUuidOnIssueChangesTable.class) + .add(4016, "Add 'type' column to 'plugins' table", AddTypeToPlugins.class) + .add(4017, "Populate 'type' column in 'plugins' table", PopulateTypeInPlugins.class) + .add(4018, "Alter 'type' column in 'plugins' to not nullable", AlterTypeInPluginNotNullable.class) ; } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPlugins.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPlugins.java new file mode 100644 index 00000000000..33a3f735119 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPlugins.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v85; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.step.DataChange; + +public class PopulateTypeInPlugins extends DataChange { + public PopulateTypeInPlugins(Database db) { + super(db); + } + + @Override + protected void execute(Context context) throws SQLException { + context.prepareUpsert("update plugins set type = 'EXTERNAL' where type is null") + .execute() + .commit(); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest.java new file mode 100644 index 00000000000..243bcd51057 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v85; + +import java.sql.SQLException; +import java.sql.Types; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.step.MigrationStep; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AddTypeToPluginsTest { + private static final String TABLE_NAME = "plugins"; + + @Rule + public CoreDbTester db = CoreDbTester.createForSchema(AddTypeToPluginsTest.class, "schema.sql"); + + private MigrationStep underTest = new AddTypeToPlugins(db.database()); + + @Test + public void add_column() throws SQLException { + addPlugin("1"); + addPlugin("2"); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, "type", Types.VARCHAR, 10, true); + assertThat(db.countRowsOfTable(TABLE_NAME)).isEqualTo(2); + } + + private void addPlugin(String id) { + db.executeInsert(TABLE_NAME, + "uuid", "uuid" + id, + "kee", "kee" + id, + "base_plugin_key", "base" + id, + "file_hash", "hash" + id, + "created_at", 1L, + "updated_at", 2L); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest.java new file mode 100644 index 00000000000..c08e6f806f1 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v85; + +import java.sql.SQLException; +import java.sql.Types; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.step.MigrationStep; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AlterTypeInPluginNotNullableTest { + private static final String TABLE_NAME = "plugins"; + + @Rule + public CoreDbTester db = CoreDbTester.createForSchema(AlterTypeInPluginNotNullableTest.class, "schema.sql"); + + private MigrationStep underTest = new AlterTypeInPluginNotNullable(db.database()); + + @Test + public void add_column() throws SQLException { + addPlugin("1"); + addPlugin("2"); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, "type", Types.VARCHAR, 10, false); + assertThat(db.countRowsOfTable(TABLE_NAME)).isEqualTo(2); + } + + private void addPlugin(String id) { + db.executeInsert(TABLE_NAME, + "uuid", "uuid" + id, + "kee", "kee" + id, + "base_plugin_key", "base" + id, + "file_hash", "hash" + id, + "type", "BUNDLED", + "created_at", 1L, + "updated_at", 2L); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest.java new file mode 100644 index 00000000000..7fe72413292 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest.java @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v85; + +import java.sql.SQLException; +import java.sql.Types; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.step.MigrationStep; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PopulateTypeInPluginsTest { + private static final String TABLE_NAME = "plugins"; + + @Rule + public CoreDbTester db = CoreDbTester.createForSchema(PopulateTypeInPluginsTest.class, "schema.sql"); + + private MigrationStep underTest = new PopulateTypeInPlugins(db.database()); + + @Test + public void add_column() throws SQLException { + addPlugin("1", null); + addPlugin("2", null); + addPlugin("3", "BUNDLED"); + + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, "type", Types.VARCHAR, 10, true); + assertThat(db.countRowsOfTable(TABLE_NAME)).isEqualTo(3); + assertThat(db.select("select type as \"TYPE\" from plugins order by uuid").stream().map(r -> r.get("TYPE"))).containsExactly("EXTERNAL", "EXTERNAL", "BUNDLED"); + } + + private void addPlugin(String id, @Nullable String type) { + db.executeInsert(TABLE_NAME, + "uuid", "uuid" + id, + "kee", "kee" + id, + "base_plugin_key", "base" + id, + "file_hash", "hash" + id, + "type", type, + "created_at", 1L, + "updated_at", 2L); + } +} diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest/schema.sql new file mode 100644 index 00000000000..c2a001fefdd --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE "PLUGINS"( + "UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(200) NOT NULL, + "BASE_PLUGIN_KEY" VARCHAR(200), + "FILE_HASH" VARCHAR(200) NOT NULL, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PLUGINS" ADD CONSTRAINT "PK_PLUGINS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "PLUGINS_KEY" ON "PLUGINS"("KEE"); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest/schema.sql new file mode 100644 index 00000000000..3d438e8eaa8 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE "PLUGINS"( + "UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(200) NOT NULL, + "BASE_PLUGIN_KEY" VARCHAR(200), + "FILE_HASH" VARCHAR(200) NOT NULL, + "TYPE" VARCHAR(10), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PLUGINS" ADD CONSTRAINT "PK_PLUGINS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "PLUGINS_KEY" ON "PLUGINS"("KEE"); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest/schema.sql new file mode 100644 index 00000000000..3d438e8eaa8 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE "PLUGINS"( + "UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(200) NOT NULL, + "BASE_PLUGIN_KEY" VARCHAR(200), + "FILE_HASH" VARCHAR(200) NOT NULL, + "TYPE" VARCHAR(10), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PLUGINS" ADD CONSTRAINT "PK_PLUGINS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "PLUGINS_KEY" ON "PLUGINS"("KEE"); diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java index 952b36208f0..2496fd0a186 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java @@ -150,6 +150,11 @@ public class ServerExtensionInstallerTest { return pluginsMap.get(key); } + @Override + public Collection getPluginInstances() { + return pluginsMap.values(); + } + @Override public boolean hasPlugin(String key) { return pluginsMap.containsKey(key); diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginCompressor.java similarity index 68% rename from server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java rename to server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginCompressor.java index 8dd2162b053..faa2551219d 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginCompressor.java @@ -26,9 +26,6 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; import java.util.jar.JarInputStream; import java.util.jar.Pack200; @@ -38,56 +35,40 @@ import org.sonar.api.server.ServerSide; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.api.utils.log.Profiler; -import org.sonar.core.platform.PluginInfo; -import org.sonar.server.plugins.InstalledPlugin.FileAndMd5; - -import static com.google.common.base.Preconditions.checkState; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; @ServerSide -public class PluginFileSystem { +public class PluginCompressor { public static final String PROPERTY_PLUGIN_COMPRESSION_ENABLE = "sonar.pluginsCompression.enable"; - private static final Logger LOG = Loggers.get(PluginFileSystem.class); + private static final Logger LOG = Loggers.get(PluginCompressor.class); private final Configuration configuration; - private final Map installedFiles = new HashMap<>(); - public PluginFileSystem(Configuration configuration) { + public PluginCompressor(Configuration configuration) { this.configuration = configuration; } + public boolean enabled() { + return configuration.getBoolean(PROPERTY_PLUGIN_COMPRESSION_ENABLE).orElse(false); + } + /** - * @param plugin - * @param loadedJar the JAR loaded by classloaders. It differs from {@code plugin.getJarFile()} + * @param loadedJar the JAR loaded by classloaders. It differs from {@code jar} * which is the initial location of JAR as seen by users */ - public void addInstalledPlugin(PluginInfo plugin, File loadedJar) { - checkState(!installedFiles.containsKey(plugin.getKey()), "Plugin %s is already loaded", plugin.getKey()); - checkState(loadedJar.exists(), "loadedJar does not exist: %s", loadedJar); - - Optional compressed = compressJar(plugin, loadedJar); - InstalledPlugin installedFile = new InstalledPlugin( - plugin, - new FileAndMd5(loadedJar), - compressed.map(FileAndMd5::new).orElse(null)); - installedFiles.put(plugin.getKey(), installedFile); - } - - public Optional getInstalledPlugin(String pluginKey) { - return Optional.ofNullable(installedFiles.get(pluginKey)); - } - - public Collection getInstalledFiles() { - return installedFiles.values(); + public PluginFilesAndMd5 compress(String key, File jar, File loadedJar) { + Optional compressed = compressJar(key, jar, loadedJar); + return new PluginFilesAndMd5(new FileAndMd5(loadedJar), compressed.map(FileAndMd5::new).orElse(null)); } - private Optional compressJar(PluginInfo plugin, File jar) { + private Optional compressJar(String key, File jar, File loadedJar) { if (!configuration.getBoolean(PROPERTY_PLUGIN_COMPRESSION_ENABLE).orElse(false)) { return Optional.empty(); } - Path targetPack200 = getPack200Path(jar.toPath()); - Path sourcePack200Path = getPack200Path(plugin.getNonNullJarFile().toPath()); + Path targetPack200 = getPack200Path(loadedJar.toPath()); + Path sourcePack200Path = getPack200Path(jar.toPath()); // check if packed file was deployed alongside the jar. If that's the case, use it instead of generating it (SONAR-10395). if (sourcePack200Path.toFile().exists()) { @@ -98,7 +79,7 @@ public class PluginFileSystem { throw new IllegalStateException("Failed to copy pack200 file from " + sourcePack200Path + " to " + targetPack200, e); } } else { - pack200(jar.toPath(), targetPack200, plugin.getKey()); + pack200(loadedJar.toPath(), targetPack200, key); } return Optional.of(targetPack200.toFile()); } diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java index 4c9a9e68fa3..4883c17f69b 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java @@ -19,15 +19,14 @@ */ package org.sonar.server.plugins; -import com.google.common.base.Optional; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.apache.commons.io.FileUtils; import org.picocontainer.Startable; import org.sonar.api.utils.HttpDownloader; @@ -99,14 +98,6 @@ public class PluginDownloader implements Startable { } } - public List getDownloadedPluginFilenames() { - List names = new ArrayList<>(); - for (File file : listPlugins(this.downloadDir)) { - names.add(file.getName()); - } - return names; - } - /** * @return the list of download plugins as {@link PluginInfo} instances */ diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFilesAndMd5.java similarity index 84% rename from server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java rename to server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFilesAndMd5.java index f9d899351c4..338f1a2311a 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFilesAndMd5.java @@ -26,27 +26,20 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; -import org.sonar.core.platform.PluginInfo; import static java.util.Objects.requireNonNull; @Immutable -public class InstalledPlugin { - private final PluginInfo plugin; +public class PluginFilesAndMd5 { private final FileAndMd5 loadedJar; @Nullable private final FileAndMd5 compressedJar; - public InstalledPlugin(PluginInfo plugin, FileAndMd5 loadedJar, @Nullable FileAndMd5 compressedJar) { - this.plugin = requireNonNull(plugin); + public PluginFilesAndMd5(FileAndMd5 loadedJar, @Nullable FileAndMd5 compressedJar) { this.loadedJar = requireNonNull(loadedJar); this.compressedJar = compressedJar; } - public PluginInfo getPluginInfo() { - return plugin; - } - public FileAndMd5 getLoadedJar() { return loadedJar; } @@ -57,7 +50,7 @@ public class InstalledPlugin { } @Immutable - public static final class FileAndMd5 { + public static class FileAndMd5 { private final File file; private final String md5; diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java new file mode 100644 index 00000000000..e604d9c434d --- /dev/null +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java @@ -0,0 +1,251 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.io.FileUtils; +import org.sonar.api.SonarRuntime; +import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.platform.PluginInfo; +import org.sonar.server.platform.ServerFileSystem; +import org.sonar.updatecenter.common.Version; + +import static java.lang.String.format; +import static org.apache.commons.io.FileUtils.moveFile; +import static org.sonar.core.util.FileUtils.deleteQuietly; +import static org.sonar.server.plugins.PluginType.BUNDLED; +import static org.sonar.server.plugins.PluginType.EXTERNAL; + +public class PluginJarLoader { + private static final Logger LOG = Loggers.get(PluginJarLoader.class); + + // List of plugins that are silently removed if installed + private static final Set DEFAULT_BLACKLISTED_PLUGINS = ImmutableSet.of("scmactivity", "issuesreport", "genericcoverage"); + // List of plugins that should prevent the server to finish its startup + private static final Set FORBIDDEN_COMPATIBLE_PLUGINS = ImmutableSet.of("sqale", "report", "views"); + + private final ServerFileSystem fs; + private final SonarRuntime runtime; + private final Set blacklistedPluginKeys; + + public PluginJarLoader(ServerFileSystem fs, SonarRuntime runtime) { + this(fs, runtime, DEFAULT_BLACKLISTED_PLUGINS); + } + + PluginJarLoader(ServerFileSystem fs, SonarRuntime runtime, Set blacklistedPluginKeys) { + this.fs = fs; + this.runtime = runtime; + this.blacklistedPluginKeys = blacklistedPluginKeys; + } + + /** + * Load the plugins that are located in lib/extensions and extensions/plugins. Blacklisted plugins are deleted. + */ + public Collection loadPlugins() { + Map bundledPluginsByKey = new LinkedHashMap<>(); + for (ServerPluginInfo bundled : getBundledPluginsMetadata()) { + failIfContains(bundledPluginsByKey, bundled, plugin -> + MessageException.of(format("Found two versions of the plugin %s [%s] in the directory %s. Please remove one of %s or %s.", + bundled.getName(), bundled.getKey(), getRelativeDir(fs.getInstalledBundledPluginsDir()), bundled.getNonNullJarFile().getName(), plugin.getNonNullJarFile().getName()))); + bundledPluginsByKey.put(bundled.getKey(), bundled); + } + + Map externalPluginsByKey = new LinkedHashMap<>(); + for (ServerPluginInfo external : getExternalPluginsMetadata()) { + failIfContains(bundledPluginsByKey, external, plugin -> + MessageException.of(format("Found a plugin '%s' in the directory %s with the same key [%s] as a bundled plugin '%s'. Please remove %s.", + external.getName(), getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getKey(), plugin.getName(), external.getNonNullJarFile().getName()))); + failIfContains(externalPluginsByKey, external, plugin -> + MessageException.of(format("Found two versions of the plugin '%s' [%s] in the directory %s. Please remove %s or %s.", external.getName(), external.getKey(), + getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getNonNullJarFile().getName(), plugin.getNonNullJarFile().getName()))); + externalPluginsByKey.put(external.getKey(), external); + } + + for (PluginInfo downloaded : getDownloadedPluginsMetadata()) { + failIfContains(bundledPluginsByKey, downloaded, plugin -> + MessageException.of(format("Fail to update plugin: %s. Bundled plugin with same key already exists: %s. Move or delete plugin from %s directory", + plugin.getName(), plugin.getKey(), getRelativeDir(fs.getDownloadedPluginsDir())))); + + ServerPluginInfo installedPlugin; + if (externalPluginsByKey.containsKey(downloaded.getKey())) { + deleteQuietly(externalPluginsByKey.get(downloaded.getKey()).getNonNullJarFile()); + installedPlugin = moveDownloadedPluginToExtensions(downloaded); + LOG.info("Plugin {} [{}] updated to version {}", installedPlugin.getName(), installedPlugin.getKey(), installedPlugin.getVersion()); + } else { + installedPlugin = moveDownloadedPluginToExtensions(downloaded); + LOG.info("Plugin {} [{}] installed", installedPlugin.getName(), installedPlugin.getKey()); + } + + externalPluginsByKey.put(downloaded.getKey(), installedPlugin); + } + + Map plugins = new HashMap<>(externalPluginsByKey.size() + bundledPluginsByKey.size()); + plugins.putAll(externalPluginsByKey); + plugins.putAll(bundledPluginsByKey); + + unloadIncompatiblePlugins(plugins); + + return plugins.values(); + } + + /** + * Removes the plugins that are not compatible with current environment. + */ + private static void unloadIncompatiblePlugins(Map pluginsByKey) { + // loop as long as the previous loop ignored some plugins. That allows to support dependencies + // on many levels, for example D extends C, which extends B, which requires A. If A is not installed, + // then B, C and D must be ignored. That's not possible to achieve this algorithm with a single iteration over plugins. + Set removedKeys = new HashSet<>(); + do { + removedKeys.clear(); + for (PluginInfo plugin : pluginsByKey.values()) { + if (!isCompatible(plugin, pluginsByKey)) { + removedKeys.add(plugin.getKey()); + } + } + for (String removedKey : removedKeys) { + pluginsByKey.remove(removedKey); + } + } while (!removedKeys.isEmpty()); + } + + @VisibleForTesting + static boolean isCompatible(PluginInfo plugin, Map allPluginsByKeys) { + if (!Strings.isNullOrEmpty(plugin.getBasePlugin()) && !allPluginsByKeys.containsKey(plugin.getBasePlugin())) { + // it extends a plugin that is not installed + LOG.warn("Plugin {} [{}] is ignored because its base plugin [{}] is not installed", plugin.getName(), plugin.getKey(), plugin.getBasePlugin()); + return false; + } + + for (PluginInfo.RequiredPlugin requiredPlugin : plugin.getRequiredPlugins()) { + PluginInfo installedRequirement = allPluginsByKeys.get(requiredPlugin.getKey()); + if (installedRequirement == null) { + // it requires a plugin that is not installed + LOG.warn("Plugin {} [{}] is ignored because the required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), requiredPlugin.getKey()); + return false; + } + Version installedRequirementVersion = installedRequirement.getVersion(); + if (installedRequirementVersion != null && requiredPlugin.getMinimalVersion().compareToIgnoreQualifier(installedRequirementVersion) > 0) { + // it requires a more recent version + LOG.warn("Plugin {} [{}] is ignored because the version {} of required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), + requiredPlugin.getMinimalVersion(), requiredPlugin.getKey()); + return false; + } + } + return true; + } + + private static String getRelativeDir(File dir) { + Path parent = dir.toPath().getParent().getParent(); + return parent.relativize(dir.toPath()).toString(); + } + + private static void failIfContains(Map map, PluginInfo value, Function msg) { + PluginInfo pluginInfo = map.get(value.getKey()); + if (pluginInfo != null) { + throw msg.apply(pluginInfo); + } + } + + private List getBundledPluginsMetadata() { + return loadPluginsFromDir(fs.getInstalledBundledPluginsDir(), jar -> ServerPluginInfo.create(jar, BUNDLED)); + } + + private List getExternalPluginsMetadata() { + return loadPluginsFromDir(fs.getInstalledExternalPluginsDir(), jar -> ServerPluginInfo.create(jar, EXTERNAL)); + } + + private List getDownloadedPluginsMetadata() { + return loadPluginsFromDir(fs.getDownloadedPluginsDir(), PluginInfo::create); + } + + private ServerPluginInfo moveDownloadedPluginToExtensions(PluginInfo pluginInfo) { + File destDir = fs.getInstalledExternalPluginsDir(); + File destFile = new File(destDir, pluginInfo.getNonNullJarFile().getName()); + if (destFile.exists()) { + deleteQuietly(destFile); + } + + movePlugin(pluginInfo.getNonNullJarFile(), destFile); + return ServerPluginInfo.create(destFile, EXTERNAL); + } + + private static void movePlugin(File sourcePluginFile, File destPluginFile) { + try { + moveFile(sourcePluginFile, destPluginFile); + } catch (IOException e) { + throw new IllegalStateException(format("Fail to move plugin: %s to %s", sourcePluginFile.getAbsolutePath(), destPluginFile.getAbsolutePath()), e); + } + } + + private List loadPluginsFromDir(File pluginsDir, Function f) { + return listJarFiles(pluginsDir).stream() + .map(f) + .filter(this::checkPluginInfo) + .collect(Collectors.toList()); + } + + private boolean checkPluginInfo(PluginInfo info) { + String pluginKey = info.getKey(); + if (blacklistedPluginKeys.contains(pluginKey)) { + LOG.warn("Plugin {} [{}] is blacklisted and is being uninstalled", info.getName(), pluginKey); + deleteQuietly(info.getNonNullJarFile()); + return false; + } + if (FORBIDDEN_COMPATIBLE_PLUGINS.contains(pluginKey)) { + throw MessageException.of(String.format("Plugin '%s' is no longer compatible with this version of SonarQube", pluginKey)); + } + + if (Strings.isNullOrEmpty(info.getMainClass()) && Strings.isNullOrEmpty(info.getBasePlugin())) { + LOG.warn("Plugin {} [{}] is ignored because entry point class is not defined", info.getName(), info.getKey()); + return false; + } + + if (!info.isCompatibleWith(runtime.getApiVersion().toString())) { + throw MessageException.of(format("Plugin %s [%s] requires at least SonarQube %s", info.getName(), info.getKey(), info.getMinimalSqVersion())); + } + return true; + } + + private static Collection listJarFiles(File dir) { + if (dir.exists()) { + return FileUtils.listFiles(dir, new String[] {"jar"}, false); + } + return Collections.emptyList(); + } + +} diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginType.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginType.java new file mode 100644 index 00000000000..0621c10677a --- /dev/null +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginType.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +public enum PluginType { + EXTERNAL, BUNDLED +} diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java index f734a346440..5c1e7c88203 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java @@ -23,38 +23,39 @@ import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import org.apache.commons.io.FileUtils; import org.picocontainer.Startable; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; import org.sonar.core.platform.PluginInfo; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.server.platform.ServerFileSystem; import static java.lang.String.format; import static org.apache.commons.io.FileUtils.forceMkdir; +import static org.apache.commons.io.FileUtils.moveFileToDirectory; +import static org.sonar.server.plugins.PluginType.EXTERNAL; public class PluginUninstaller implements Startable { + private static final Logger LOG = Loggers.get(PluginUninstaller.class); private static final String PLUGIN_EXTENSION = "jar"; - private final ServerPluginRepository serverPluginRepository; - private final File uninstallDir; - public PluginUninstaller(ServerPluginRepository serverPluginRepository, ServerFileSystem fs) { - this.serverPluginRepository = serverPluginRepository; - this.uninstallDir = fs.getUninstalledPluginsDir(); - } + private final ServerFileSystem fs; + private final ServerPluginRepository pluginRepository; - private static Collection listJarFiles(File dir) { - if (dir.exists()) { - return FileUtils.listFiles(dir, new String[] {PLUGIN_EXTENSION}, false); - } - return Collections.emptyList(); + public PluginUninstaller(ServerFileSystem fs, ServerPluginRepository pluginRepository) { + this.fs = fs; + this.pluginRepository = pluginRepository; } @Override public void start() { try { - forceMkdir(uninstallDir); + forceMkdir(fs.getUninstalledPluginsDir()); } catch (IOException e) { - throw new IllegalStateException("Fail to create the directory: " + uninstallDir, e); + throw new IllegalStateException("Fail to create the directory: " + fs.getUninstalledPluginsDir(), e); } } @@ -63,28 +64,84 @@ public class PluginUninstaller implements Startable { // Nothing to do } + /** + * Uninstall a plugin and its dependents + */ public void uninstall(String pluginKey) { - ensurePluginIsInstalled(pluginKey); - serverPluginRepository.uninstall(pluginKey, uninstallDir); + if (!pluginRepository.hasPlugin(pluginKey) || pluginRepository.getPlugin(pluginKey).getType() != EXTERNAL) { + throw new IllegalArgumentException(format("Plugin [%s] is not installed", pluginKey)); + } + + Set uninstallKeys = new HashSet<>(); + uninstallKeys.add(pluginKey); + appendDependentPluginKeys(pluginKey, uninstallKeys); + + for (String uninstallKey : uninstallKeys) { + PluginInfo info = pluginRepository.getPluginInfo(uninstallKey); + // we don't check type because the dependent of an external plugin should never be a bundled plugin! + uninstall(info.getKey(), info.getName(), info.getNonNullJarFile().getName()); + } } public void cancelUninstalls() { - serverPluginRepository.cancelUninstalls(uninstallDir); + for (File file : listJarFiles(fs.getUninstalledPluginsDir())) { + try { + moveFileToDirectory(file, fs.getInstalledExternalPluginsDir(), false); + } catch (IOException e) { + throw new IllegalStateException("Fail to cancel plugin uninstalls", e); + } + } } /** * @return the list of plugins to be uninstalled as {@link PluginInfo} instances */ public Collection getUninstalledPlugins() { - return listJarFiles(uninstallDir) - .stream() + return listJarFiles(fs.getUninstalledPluginsDir()).stream() .map(PluginInfo::create) .collect(MoreCollectors.toList()); } - private void ensurePluginIsInstalled(String key) { - if (!serverPluginRepository.hasPlugin(key)) { - throw new IllegalArgumentException(format("Plugin [%s] is not installed", key)); + private static Collection listJarFiles(File dir) { + if (dir.exists()) { + return FileUtils.listFiles(dir, new String[] {PLUGIN_EXTENSION}, false); + } + return Collections.emptyList(); + } + + private void uninstall(String key, String name, String fileName) { + try { + if (!getPluginFile(fileName).exists()) { + LOG.info("Plugin already uninstalled: {} [{}]", name, key); + return; + } + + LOG.info("Uninstalling plugin {} [{}]", name, key); + + File masterFile = getPluginFile(fileName); + moveFileToDirectory(masterFile, fs.getUninstalledPluginsDir(), true); + } catch (IOException e) { + throw new IllegalStateException(format("Fail to uninstall plugin %s [%s]", name, key), e); + } + } + + private File getPluginFile(String fileName) { + // just to be sure that file is located in from extensions/plugins + return new File(fs.getInstalledExternalPluginsDir(), fileName); + } + + private void appendDependentPluginKeys(String pluginKey, Set appendTo) { + for (PluginInfo otherPlugin : pluginRepository.getPluginInfos()) { + if (otherPlugin.getKey().equals(pluginKey)) { + continue; + } + + for (PluginInfo.RequiredPlugin requirement : otherPlugin.getRequiredPlugins()) { + if (requirement.getKey().equals(pluginKey)) { + appendTo.add(otherPlugin.getKey()); + appendDependentPluginKeys(otherPlugin.getKey(), appendTo); + } + } } } } diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPlugin.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPlugin.java new file mode 100644 index 00000000000..8bffd9d41c0 --- /dev/null +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPlugin.java @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.Plugin; +import org.sonar.core.platform.PluginInfo; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; + +public class ServerPlugin { + private final PluginInfo pluginInfo; + private final PluginType type; + private final Plugin instance; + private final FileAndMd5 jar; + private final FileAndMd5 compressed; + private final ClassLoader classloader; + + public ServerPlugin(PluginInfo pluginInfo, PluginType type, Plugin instance, FileAndMd5 jar, @Nullable FileAndMd5 compressed) { + this(pluginInfo, type, instance, jar, compressed, instance.getClass().getClassLoader()); + } + + public ServerPlugin(PluginInfo pluginInfo, PluginType type, Plugin instance, FileAndMd5 jar, @Nullable FileAndMd5 compressed, ClassLoader classloader) { + this.pluginInfo = pluginInfo; + this.type = type; + this.instance = instance; + this.jar = jar; + this.compressed = compressed; + this.classloader = classloader; + } + + public PluginInfo getPluginInfo() { + return pluginInfo; + } + + public Plugin getInstance() { + return instance; + } + + public PluginType getType() { + return type; + } + + public FileAndMd5 getJar() { + return jar; + } + + @CheckForNull + public FileAndMd5 getCompressed() { + return compressed; + } + + public ClassLoader getClassloader() { + return classloader; + } +} diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginInfo.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginInfo.java new file mode 100644 index 00000000000..04696020ade --- /dev/null +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginInfo.java @@ -0,0 +1,79 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import org.sonar.core.platform.PluginInfo; +import org.sonar.updatecenter.common.PluginManifest; + +public class ServerPluginInfo extends PluginInfo { + private PluginType type; + + public ServerPluginInfo(String key) { + super(key); + } + + public static ServerPluginInfo create(File jarFile, PluginType type) { + try { + PluginManifest manifest = new PluginManifest(jarFile); + ServerPluginInfo serverPluginInfo = new ServerPluginInfo(manifest.getKey()); + serverPluginInfo.fillFields(jarFile, manifest, type); + return serverPluginInfo; + } catch (IOException e) { + throw new IllegalStateException("Fail to extract plugin metadata from file: " + jarFile, e); + } + } + + private void fillFields(File jarFile, PluginManifest manifest, PluginType type) { + super.fillFields(jarFile, manifest); + setType(type); + } + + public PluginType getType() { + return type; + } + + public ServerPluginInfo setType(PluginType type) { + this.type = type; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + ServerPluginInfo that = (ServerPluginInfo) o; + return type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type); + } +} diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java index 481251fb121..5612fdaa0e1 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java @@ -24,6 +24,7 @@ import org.apache.commons.io.FileUtils; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.ZipUtils; import org.sonar.core.platform.ExplodedPlugin; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.core.platform.PluginInfo; import org.sonar.core.platform.PluginJarExploder; import org.sonar.server.platform.ServerFileSystem; @@ -33,36 +34,31 @@ import static org.apache.commons.io.FileUtils.forceMkdir; @ServerSide public class ServerPluginJarExploder extends PluginJarExploder { private final ServerFileSystem fs; - private final PluginFileSystem pluginFileSystem; - public ServerPluginJarExploder(ServerFileSystem fs, PluginFileSystem pluginFileSystem) { + public ServerPluginJarExploder(ServerFileSystem fs) { this.fs = fs; - this.pluginFileSystem = pluginFileSystem; } /** * JAR files of directory extensions/plugins can be moved when server is up and plugins are uninstalled. * For this reason these files must not be locked by classloaders. They are copied to the directory - * web/deploy/plugins in order to be loaded by {@link org.sonar.core.platform.PluginLoader}. + * web/deploy/plugins in order to be loaded by {@link PluginClassLoader}. */ @Override - public ExplodedPlugin explode(PluginInfo pluginInfo) { - File toDir = new File(fs.getDeployedPluginsDir(), pluginInfo.getKey()); + public ExplodedPlugin explode(PluginInfo plugin) { + File toDir = new File(fs.getDeployedPluginsDir(), plugin.getKey()); try { forceMkdir(toDir); org.sonar.core.util.FileUtils.cleanDirectory(toDir); - File jarSource = pluginInfo.getNonNullJarFile(); - File jarTarget = new File(toDir, jarSource.getName()); + File jarTarget = new File(toDir, plugin.getNonNullJarFile().getName()); - FileUtils.copyFile(jarSource, jarTarget); - ZipUtils.unzip(jarSource, toDir, newLibFilter()); - ExplodedPlugin explodedPlugin = explodeFromUnzippedDir(pluginInfo.getKey(), jarTarget, toDir); - pluginFileSystem.addInstalledPlugin(pluginInfo, jarTarget); - return explodedPlugin; + FileUtils.copyFile(plugin.getNonNullJarFile(), jarTarget); + ZipUtils.unzip(plugin.getNonNullJarFile(), toDir, newLibFilter()); + return explodeFromUnzippedDir(plugin, jarTarget, toDir); } catch (Exception e) { throw new IllegalStateException(String.format( - "Fail to unzip plugin [%s] %s to %s", pluginInfo.getKey(), pluginInfo.getNonNullJarFile().getAbsolutePath(), toDir.getAbsolutePath()), e); + "Fail to unzip plugin [%s] %s to %s", plugin.getKey(), plugin.getNonNullJarFile().getAbsolutePath(), toDir.getAbsolutePath()), e); } } } diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginManager.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginManager.java new file mode 100644 index 00000000000..43f90ba0f01 --- /dev/null +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginManager.java @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.picocontainer.Startable; +import org.sonar.api.Plugin; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.platform.ExplodedPlugin; +import org.sonar.core.platform.PluginClassLoader; +import org.sonar.core.platform.PluginJarExploder; + +/** + * Entry point to install and load plugins on server startup. It manages + *
    + *
  • installation of new plugins (effective after server startup)
  • + *
  • un-installation of plugins (effective after server startup)
  • + *
  • cancel pending installations/un-installations
  • + *
  • instantiation of plugin entry-points
  • + *
+ */ +public class ServerPluginManager implements Startable { + private static final Logger LOG = Loggers.get(ServerPluginManager.class); + + private final PluginJarLoader pluginJarLoader; + private final PluginJarExploder pluginJarExploder; + private final PluginClassLoader pluginClassLoader; + private final PluginCompressor pluginCompressor; + private final ServerPluginRepository pluginRepository; + + public ServerPluginManager(PluginClassLoader pluginClassLoader, PluginJarExploder pluginJarExploder, + PluginJarLoader pluginJarLoader, PluginCompressor pluginCompressor, ServerPluginRepository pluginRepository) { + this.pluginClassLoader = pluginClassLoader; + this.pluginJarExploder = pluginJarExploder; + this.pluginJarLoader = pluginJarLoader; + this.pluginCompressor = pluginCompressor; + this.pluginRepository = pluginRepository; + } + + @Override + public void start() { + Collection loadedPlugins = pluginJarLoader.loadPlugins(); + logInstalledPlugins(loadedPlugins); + Collection explodedPlugins = extractPlugins(loadedPlugins); + Map instancesByKey = pluginClassLoader.load(explodedPlugins); + Map typesByKey = getTypesByKey(loadedPlugins); + List plugins = compressAndCreateServerPlugins(explodedPlugins, instancesByKey, typesByKey); + pluginRepository.addPlugins(plugins); + } + + private static Map getTypesByKey(Collection loadedPlugins) { + return loadedPlugins.stream().collect(Collectors.toMap(ServerPluginInfo::getKey, ServerPluginInfo::getType)); + } + + @Override + public void stop() { + pluginClassLoader.unload(pluginRepository.getPluginInstances()); + } + + private static void logInstalledPlugins(Collection plugins) { + plugins.stream().sorted().forEach(plugin -> LOG.info("Deploy plugin {} / {} / {}", plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild())); + } + + private Collection extractPlugins(Collection plugins) { + return plugins.stream().map(pluginJarExploder::explode).collect(Collectors.toList()); + } + + private List compressAndCreateServerPlugins(Collection explodedPlugins, Map instancesByKey, Map typseByKey) { + List plugins = new ArrayList<>(); + for (ExplodedPlugin p : explodedPlugins) { + PluginFilesAndMd5 installedPlugin = pluginCompressor.compress(p.getKey(), p.getPluginInfo().getNonNullJarFile(), p.getMain()); + plugins.add(new ServerPlugin(p.getPluginInfo(), typseByKey.get(p.getKey()), instancesByKey.get(p.getKey()), + installedPlugin.getLoadedJar(), installedPlugin.getCompressedJar())); + } + return plugins; + } +} diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java index 33b03519040..2b2863f8a3c 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java @@ -19,382 +19,86 @@ */ package org.sonar.server.plugins; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Ordering; -import java.io.File; -import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Optional; +import java.util.stream.Collectors; import javax.annotation.CheckForNull; -import org.apache.commons.io.FileUtils; -import org.picocontainer.Startable; import org.sonar.api.Plugin; -import org.sonar.api.SonarRuntime; -import org.sonar.api.utils.MessageException; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; import org.sonar.core.platform.PluginInfo; -import org.sonar.core.platform.PluginLoader; import org.sonar.core.platform.PluginRepository; -import org.sonar.server.platform.ServerFileSystem; -import org.sonar.updatecenter.common.Version; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; import static java.lang.String.format; -import static org.apache.commons.io.FileUtils.moveFile; -import static org.apache.commons.io.FileUtils.moveFileToDirectory; -import static org.sonar.core.util.FileUtils.deleteQuietly; -/** - * Entry point to install and load plugins on server startup. It manages - *
    - *
  • installation of new plugins (effective after server startup)
  • - *
  • un-installation of plugins (effective after server startup)
  • - *
  • cancel pending installations/un-installations
  • - *
  • instantiation of plugin entry-points
  • - *
- */ -public class ServerPluginRepository implements PluginRepository, Startable { - - private static final Logger LOG = Loggers.get(ServerPluginRepository.class); - private static final String[] JAR_FILE_EXTENSIONS = new String[] {"jar"}; - // List of plugins that are silently removed if installed - private static final Set DEFAULT_BLACKLISTED_PLUGINS = ImmutableSet.of("scmactivity", "issuesreport", "genericcoverage"); - // List of plugins that should prevent the server to finish its startup - private static final Set FORBIDDEN_COMPATIBLE_PLUGINS = ImmutableSet.of("sqale", "report", "views"); - private static final Joiner SLASH_JOINER = Joiner.on(" / ").skipNulls(); - private static final String NOT_STARTED_YET = "not started yet"; - - private final SonarRuntime runtime; - private final ServerFileSystem fs; - private final PluginLoader loader; - private final AtomicBoolean started = new AtomicBoolean(false); - private Set blacklistedPluginKeys = DEFAULT_BLACKLISTED_PLUGINS; - - // following fields are available after startup - private final Map pluginInfosByKeys = new HashMap<>(); - private final Map pluginInstancesByKeys = new HashMap<>(); +public class ServerPluginRepository implements PluginRepository { + private final Map pluginByKey = new HashMap<>(); private final Map keysByClassLoader = new HashMap<>(); - public ServerPluginRepository(SonarRuntime runtime, ServerFileSystem fs, PluginLoader loader) { - this.runtime = runtime; - this.fs = fs; - this.loader = loader; - } - - @VisibleForTesting - void setBlacklistedPluginKeys(Set keys) { - this.blacklistedPluginKeys = keys; - } - - @Override - public void start() { - loadPreInstalledPlugins(); - moveDownloadedPlugins(); - unloadIncompatiblePlugins(); - logInstalledPlugins(); - loadInstances(); - started.set(true); + public void addPlugins(List plugins) { + pluginByKey.putAll(plugins.stream().collect(Collectors.toMap(p -> p.getPluginInfo().getKey(), p -> p))); + for (ServerPlugin p : plugins) { + keysByClassLoader.put(p.getClassloader(), p.getPluginInfo().getKey()); + } } - @Override - public void stop() { - // close classloaders - loader.unload(pluginInstancesByKeys.values()); - pluginInstancesByKeys.clear(); - pluginInfosByKeys.clear(); - keysByClassLoader.clear(); - started.set(true); + public void addPlugin(ServerPlugin plugin) { + pluginByKey.put(plugin.getPluginInfo().getKey(), plugin); + if (plugin.getInstance() != null) { + keysByClassLoader.put(plugin.getInstance().getClass().getClassLoader(), plugin.getPluginInfo().getKey()); + } } - /** - * Return the key of the plugin the extension (in the sense of {@link Plugin.Context#addExtension(Object)} is coming from. - */ @CheckForNull public String getPluginKey(Object extension) { return keysByClassLoader.get(extension.getClass().getClassLoader()); } - /** - * Load the plugins that are located in lib/extensions and extensions/plugins. Blacklisted plugins are - * deleted. - */ - private void loadPreInstalledPlugins() { - registerPluginsFromDir(fs.getInstalledBundledPluginsDir()); - registerPluginsFromDir(fs.getInstalledExternalPluginsDir()); - } - - private void registerPluginsFromDir(File pluginsDir) { - for (File file : listJarFiles(pluginsDir)) { - PluginInfo info = PluginInfo.create(file); - registerPluginInfo(info); - } - } - - /** - * Move the plugins recently downloaded to extensions/plugins. - */ - private void moveDownloadedPlugins() { - if (fs.getDownloadedPluginsDir().exists()) { - for (File sourceFile : listJarFiles(fs.getDownloadedPluginsDir())) { - overrideAndRegisterPlugin(sourceFile); - } - } - } - - private void registerPluginInfo(PluginInfo info) { - String pluginKey = info.getKey(); - if (blacklistedPluginKeys.contains(pluginKey)) { - LOG.warn("Plugin {} [{}] is blacklisted and is being uninstalled", info.getName(), pluginKey); - deleteQuietly(info.getNonNullJarFile()); - return; - } - if (FORBIDDEN_COMPATIBLE_PLUGINS.contains(pluginKey)) { - throw MessageException.of(String.format("Plugin '%s' is no longer compatible with this version of SonarQube", pluginKey)); - } - PluginInfo existing = pluginInfosByKeys.put(pluginKey, info); - if (existing != null) { - File existingPluginParentDir = existing.getNonNullJarFile().getParentFile(); - File currentPluginParentDir = info.getNonNullJarFile().getParentFile(); - if (existingPluginParentDir.equals(currentPluginParentDir)) { - String directory = existingPluginParentDir.equals(fs.getInstalledBundledPluginsDir()) ? "lib/extensions" : "extensions/plugins"; - throw MessageException.of(format("Found two versions of the plugin %s [%s] in the directory %s. Please remove one of %s or %s.", - info.getName(), pluginKey, directory, info.getNonNullJarFile().getName(), existing.getNonNullJarFile().getName())); - } else { - throw MessageException - .of(format("Found two versions of the plugin %s [%s] in different directories lib/extensions and extension/plugins. Please remove the one from extension/plugins: %s.", - info.getName(), pluginKey, info.getNonNullJarFile().getName())); - } - } - } - - /** - * Move or copy plugin to directory extensions/plugins. If a version of this plugin - * already exists then it's deleted. - */ - private void overrideAndRegisterPlugin(File sourceFile) { - File destDir = fs.getInstalledExternalPluginsDir(); - File destFile = new File(destDir, sourceFile.getName()); - if (destFile.exists()) { - // plugin with same filename already installed - deleteQuietly(destFile); - } - - movePlugin(sourceFile, destFile); - - PluginInfo info = PluginInfo.create(destFile); - PluginInfo existing = pluginInfosByKeys.put(info.getKey(), info); - - if (existing != null) { - File existingJarFile = existing.getNonNullJarFile(); - - if (existingJarFile.getParentFile().equals(fs.getInstalledBundledPluginsDir())) { - // move downloaded plugin back to origin location - movePlugin(destFile, sourceFile); - throw MessageException.of(format("Fail to update plugin: %s. Bundled plugin with same key already exists: %s. " - + "Move or delete plugin from extensions/downloads directory", - sourceFile.getName(), existing.getKey())); - } - - if (!existingJarFile.getName().equals(destFile.getName())) { - deleteQuietly(existingJarFile); - } - LOG.info("Plugin {} [{}] updated to version {}", info.getName(), info.getKey(), info.getVersion()); - } else { - LOG.info("Plugin {} [{}] installed", info.getName(), info.getKey()); - } - } - - private void movePlugin(File sourcePluginFile, File destPluginFile) { - try { - moveFile(sourcePluginFile, destPluginFile); - - } catch (IOException e) { - throw new IllegalStateException(format("Fail to move plugin: %s to %s", - sourcePluginFile.getAbsolutePath(), destPluginFile.getAbsolutePath()), e); - } - } - - /** - * Removes the plugins that are not compatible with current environment. - */ - private void unloadIncompatiblePlugins() { - // loop as long as the previous loop ignored some plugins. That allows to support dependencies - // on many levels, for example D extends C, which extends B, which requires A. If A is not installed, - // then B, C and D must be ignored. That's not possible to achieve this algorithm with a single - // iteration over plugins. - Set removedKeys = new HashSet<>(); - do { - removedKeys.clear(); - for (PluginInfo plugin : pluginInfosByKeys.values()) { - if (!isCompatible(plugin, runtime, pluginInfosByKeys)) { - removedKeys.add(plugin.getKey()); - } - } - for (String removedKey : removedKeys) { - pluginInfosByKeys.remove(removedKey); - } - } while (!removedKeys.isEmpty()); - } - - @VisibleForTesting - static boolean isCompatible(PluginInfo plugin, SonarRuntime runtime, Map allPluginsByKeys) { - if (Strings.isNullOrEmpty(plugin.getMainClass()) && Strings.isNullOrEmpty(plugin.getBasePlugin())) { - LOG.warn("Plugin {} [{}] is ignored because entry point class is not defined", plugin.getName(), plugin.getKey()); - return false; - } - - if (!plugin.isCompatibleWith(runtime.getApiVersion().toString())) { - throw MessageException.of(format( - "Plugin %s [%s] requires at least SonarQube %s", plugin.getName(), plugin.getKey(), plugin.getMinimalSqVersion())); - } - - if (!Strings.isNullOrEmpty(plugin.getBasePlugin()) && !allPluginsByKeys.containsKey(plugin.getBasePlugin())) { - // it extends a plugin that is not installed - LOG.warn("Plugin {} [{}] is ignored because its base plugin [{}] is not installed", plugin.getName(), plugin.getKey(), plugin.getBasePlugin()); - return false; - } - - for (PluginInfo.RequiredPlugin requiredPlugin : plugin.getRequiredPlugins()) { - PluginInfo installedRequirement = allPluginsByKeys.get(requiredPlugin.getKey()); - if (installedRequirement == null) { - // it requires a plugin that is not installed - LOG.warn("Plugin {} [{}] is ignored because the required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), requiredPlugin.getKey()); - return false; - } - Version installedRequirementVersion = installedRequirement.getVersion(); - if (installedRequirementVersion != null && requiredPlugin.getMinimalVersion().compareToIgnoreQualifier(installedRequirementVersion) > 0) { - // it requires a more recent version - LOG.warn("Plugin {} [{}] is ignored because the version {} of required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), - requiredPlugin.getMinimalVersion(), requiredPlugin.getKey()); - return false; - } - } - return true; - } - - private void logInstalledPlugins() { - List orderedPlugins = Ordering.natural().sortedCopy(pluginInfosByKeys.values()); - for (PluginInfo plugin : orderedPlugins) { - LOG.info("Deploy plugin {}", SLASH_JOINER.join(plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild())); - } - } - - private void loadInstances() { - pluginInstancesByKeys.putAll(loader.load(pluginInfosByKeys)); - - for (Map.Entry e : pluginInstancesByKeys.entrySet()) { - keysByClassLoader.put(e.getValue().getClass().getClassLoader(), e.getKey()); - } - } - - /** - * Uninstall a plugin and its dependents - */ - public void uninstall(String pluginKey, File uninstallDir) { - Set uninstallKeys = new HashSet<>(); - uninstallKeys.add(pluginKey); - appendDependentPluginKeys(pluginKey, uninstallKeys); - - for (String uninstallKey : uninstallKeys) { - PluginInfo info = getPluginInfo(uninstallKey); - - try { - if (!getPluginFile(info).exists()) { - LOG.info("Plugin already uninstalled: {} [{}]", info.getName(), info.getKey()); - continue; - } - - LOG.info("Uninstalling plugin {} [{}]", info.getName(), info.getKey()); - - File masterFile = getPluginFile(info); - moveFileToDirectory(masterFile, uninstallDir, true); - } catch (IOException e) { - throw new IllegalStateException(format("Fail to uninstall plugin %s [%s]", info.getName(), info.getKey()), e); - } - } + @Override + public Collection getPluginInfos() { + return Collections.unmodifiableCollection(pluginByKey.values().stream().map(ServerPlugin::getPluginInfo).collect(Collectors.toList())); } - public void cancelUninstalls(File uninstallDir) { - for (File file : listJarFiles(uninstallDir)) { - try { - moveFileToDirectory(file, fs.getInstalledExternalPluginsDir(), false); - } catch (IOException e) { - throw new IllegalStateException("Fail to cancel plugin uninstalls", e); - } - } + @Override + public PluginInfo getPluginInfo(String key) { + return getPlugin(key).getPluginInfo(); } - /** - * Appends dependent plugins, only the ones that still exist in the plugins folder. - */ - private void appendDependentPluginKeys(String pluginKey, Set appendTo) { - for (PluginInfo otherPlugin : getPluginInfos()) { - if (!otherPlugin.getKey().equals(pluginKey)) { - for (PluginInfo.RequiredPlugin requirement : otherPlugin.getRequiredPlugins()) { - if (requirement.getKey().equals(pluginKey)) { - appendTo.add(otherPlugin.getKey()); - appendDependentPluginKeys(otherPlugin.getKey(), appendTo); - } - } - } + public ServerPlugin getPlugin(String key) { + ServerPlugin plugin = pluginByKey.get(key); + if (plugin == null) { + throw new IllegalArgumentException(format("Plugin [%s] does not exist", key)); } + return plugin; } - private File getPluginFile(PluginInfo info) { - // we don't reuse info.getFile() just to be sure that file is located in from extensions/plugins - return new File(fs.getInstalledExternalPluginsDir(), info.getNonNullJarFile().getName()); - } - - public Map getPluginInfosByKeys() { - return pluginInfosByKeys; - } - - @Override - public Collection getPluginInfos() { - checkState(started.get(), NOT_STARTED_YET); - return ImmutableList.copyOf(pluginInfosByKeys.values()); + public Collection getPlugins() { + return Collections.unmodifiableCollection(pluginByKey.values()); } - @Override - public PluginInfo getPluginInfo(String key) { - checkState(started.get(), NOT_STARTED_YET); - PluginInfo info = pluginInfosByKeys.get(key); - if (info == null) { - throw new IllegalArgumentException(format("Plugin [%s] does not exist", key)); - } - return info; + public Optional findPlugin(String key) { + return Optional.ofNullable(pluginByKey.get(key)); } @Override public Plugin getPluginInstance(String key) { - checkState(started.get(), NOT_STARTED_YET); - Plugin plugin = pluginInstancesByKeys.get(key); + ServerPlugin plugin = pluginByKey.get(key); checkArgument(plugin != null, "Plugin [%s] does not exist", key); - return plugin; + return plugin.getInstance(); } @Override - public boolean hasPlugin(String key) { - checkState(started.get(), NOT_STARTED_YET); - return pluginInfosByKeys.containsKey(key); + public Collection getPluginInstances() { + return pluginByKey.values().stream() + .map(ServerPlugin::getInstance) + .collect(Collectors.toList()); } - private static Collection listJarFiles(File dir) { - if (dir.exists()) { - return FileUtils.listFiles(dir, JAR_FILE_EXTENSIONS, false); - } - return Collections.emptyList(); + @Override + public boolean hasPlugin(String key) { + return pluginByKey.containsKey(key); } } diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java index 4fd62aedbad..29e1857bce8 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java @@ -19,12 +19,12 @@ */ package org.sonar.server.plugins; -import com.google.common.base.Optional; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.Date; +import java.util.Optional; import org.apache.commons.io.IOUtils; import org.sonar.api.Properties; import org.sonar.api.Property; @@ -75,14 +75,14 @@ public class UpdateCenterClient { public Optional getUpdateCenter(boolean forceRefresh) { if (!isActivated) { - return Optional.absent(); + return Optional.empty(); } if (pluginCenter == null || forceRefresh || needsRefresh()) { pluginCenter = init(); lastRefreshDate = System.currentTimeMillis(); } - return Optional.fromNullable(pluginCenter); + return Optional.ofNullable(pluginCenter); } public Date getLastRefreshDate() { diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java index 08d0b9d4592..210f2b92a78 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java @@ -19,7 +19,7 @@ */ package org.sonar.server.plugins; -import com.google.common.base.Optional; +import java.util.Optional; import org.sonar.api.SonarRuntime; import org.sonar.updatecenter.common.UpdateCenter; import org.sonar.updatecenter.common.Version; @@ -50,6 +50,6 @@ public class UpdateCenterMatrixFactory { installedPluginReferentialFactory.getInstalledPluginReferential()) .setDate(centerClient.getLastRefreshDate())); } - return Optional.absent(); + return Optional.empty(); } } diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginCompressorTest.java similarity index 73% rename from server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java rename to server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginCompressorTest.java index 98735e46f94..e32b0ddfafe 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginCompressorTest.java @@ -21,6 +21,7 @@ package org.sonar.server.plugins; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.io.FileUtils; @@ -30,12 +31,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.sonar.api.config.internal.MapSettings; -import org.sonar.core.platform.PluginInfo; import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.server.plugins.PluginFileSystem.PROPERTY_PLUGIN_COMPRESSION_ENABLE; +import static org.sonar.server.plugins.PluginCompressor.PROPERTY_PLUGIN_COMPRESSION_ENABLE; -public class PluginFileSystemTest { +public class PluginCompressorTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); @@ -43,42 +43,21 @@ public class PluginFileSystemTest { @Before public void setUp() throws IOException { - Path sourceFolder = temp.newFolder("source").toPath(); Path targetFolder = temp.newFolder("target").toPath(); Path targetJarPath = targetFolder.resolve("test.jar"); Files.createFile(targetJarPath); } - @Test - public void add_plugin_to_list_of_installed_plugins() throws IOException { - File jar = touch(temp.newFolder(), "sonar-foo-plugin.jar"); - PluginInfo info = new PluginInfo("foo"); - - PluginFileSystem underTest = new PluginFileSystem(settings.asConfig()); - underTest.addInstalledPlugin(info, jar); - - assertThat(underTest.getInstalledFiles()).hasSize(1); - InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get(); - assertThat(installedPlugin.getCompressedJar()).isNull(); - assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(jar.toPath()); - assertThat(installedPlugin.getPluginInfo()).isSameAs(info); - } - @Test public void compress_jar_if_compression_enabled() throws IOException { File jar = touch(temp.newFolder(), "sonar-foo-plugin.jar"); - PluginInfo info = new PluginInfo("foo").setJarFile(jar); // the JAR is copied somewhere else in order to be loaded by classloaders File loadedJar = touch(temp.newFolder(), "sonar-foo-plugin.jar"); settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, true); - PluginFileSystem underTest = new PluginFileSystem(settings.asConfig()); - underTest.addInstalledPlugin(info, loadedJar); - - assertThat(underTest.getInstalledFiles()).hasSize(1); + PluginCompressor underTest = new PluginCompressor(settings.asConfig()); - InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get(); - assertThat(installedPlugin.getPluginInfo()).isSameAs(info); + PluginFilesAndMd5 installedPlugin = underTest.compress("foo", jar, loadedJar); assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath()); assertThat(installedPlugin.getCompressedJar().getFile()) .exists() @@ -87,34 +66,44 @@ public class PluginFileSystemTest { .hasParent(loadedJar.getParentFile()); } + @Test + public void dont_compress_jar_if_compression_disable() throws IOException { + File jar = touch(temp.newFolder(), "sonar-foo-plugin.jar"); + // the JAR is copied somewhere else in order to be loaded by classloaders + File loadedJar = touch(temp.newFolder(), "sonar-foo-plugin.jar"); + + settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, false); + PluginCompressor underTest = new PluginCompressor(settings.asConfig()); + + PluginFilesAndMd5 installedPlugin = underTest.compress("foo", jar, loadedJar); + assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath()); + assertThat(installedPlugin.getCompressedJar()).isNull(); + assertThat(installedPlugin.getLoadedJar().getFile().getParentFile().listFiles()).containsOnly(loadedJar); + } + @Test public void copy_and_use_existing_packed_jar_if_compression_enabled() throws IOException { File jar = touch(temp.newFolder(), "sonar-foo-plugin.jar"); File packedJar = touch(jar.getParentFile(), "sonar-foo-plugin.pack.gz"); - PluginInfo info = new PluginInfo("foo").setJarFile(jar); // the JAR is copied somewhere else in order to be loaded by classloaders File loadedJar = touch(temp.newFolder(), "sonar-foo-plugin.jar"); settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, true); - PluginFileSystem underTest = new PluginFileSystem(settings.asConfig()); - underTest.addInstalledPlugin(info, loadedJar); - - assertThat(underTest.getInstalledFiles()).hasSize(1); + PluginCompressor underTest = new PluginCompressor(settings.asConfig()); - InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get(); - assertThat(installedPlugin.getPluginInfo()).isSameAs(info); + PluginFilesAndMd5 installedPlugin = underTest.compress("foo", jar, loadedJar); assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath()); assertThat(installedPlugin.getCompressedJar().getFile()) .exists() .isFile() .hasName(packedJar.getName()) .hasParent(loadedJar.getParentFile()) - .hasSameContentAs(packedJar); + .hasSameTextualContentAs(packedJar); } private static File touch(File dir, String filename) throws IOException { File file = new File(dir, filename); - FileUtils.write(file, RandomStringUtils.random(10)); + FileUtils.write(file, RandomStringUtils.random(10), StandardCharsets.UTF_8); return file; } diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java index a8319bcff55..57dee7d4e35 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java @@ -19,9 +19,9 @@ */ package org.sonar.server.plugins; -import com.google.common.base.Optional; import java.io.File; import java.net.URI; +import java.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -129,7 +129,7 @@ public class PluginDownloaderTest { @Test public void download_when_update_center_is_unavailable_with_no_exception_thrown() { - when(updateCenterMatrixFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent()); + when(updateCenterMatrixFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.empty()); Plugin test = Plugin.factory("test"); Release test10 = new Release(test, "1.0").setDownloadUrl("http://server/test-1.0.jar"); diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFilesAndMd5Test.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFilesAndMd5Test.java new file mode 100644 index 00000000000..034a7a4c06c --- /dev/null +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFilesAndMd5Test.java @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PluginFilesAndMd5Test { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void getters() throws IOException { + File jarFile = temp.newFile(); + Files.write(jarFile.toPath(), "f1".getBytes(StandardCharsets.UTF_8)); + File jarFileCompressed = temp.newFile(); + + Files.write(jarFileCompressed.toPath(), "f1compressed".getBytes(StandardCharsets.UTF_8)); + + PluginFilesAndMd5.FileAndMd5 jar = new PluginFilesAndMd5.FileAndMd5(jarFile); + PluginFilesAndMd5.FileAndMd5 jarCompressed = new PluginFilesAndMd5.FileAndMd5(jarFileCompressed); + + PluginFilesAndMd5 underTest = new PluginFilesAndMd5(jar, jarCompressed); + + assertThat(underTest.getCompressedJar().getFile()).isEqualTo(jarFileCompressed); + assertThat(underTest.getCompressedJar().getMd5()).isEqualTo("a0d076c0fc9f11ec68740fed5aa3ce38"); + + assertThat(underTest.getLoadedJar().getFile()).isEqualTo(jarFile); + assertThat(underTest.getLoadedJar().getMd5()).isEqualTo("bd19836ddb62c11c55ab251ccaca5645"); + } + + @Test + public void fail_if_cant_get_md5() throws IOException { + File jarFile = new File("nonexisting"); + Assert.assertThrows("Fail to compute md5", IllegalStateException.class, () -> new PluginFilesAndMd5.FileAndMd5(jarFile)); + } +} diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginJarLoaderTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginJarLoaderTest.java new file mode 100644 index 00000000000..aec253e4feb --- /dev/null +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginJarLoaderTest.java @@ -0,0 +1,319 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import javax.annotation.Nullable; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.SonarRuntime; +import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.log.LogTester; +import org.sonar.core.platform.PluginInfo; +import org.sonar.server.platform.ServerFileSystem; +import org.sonar.updatecenter.common.PluginManifest; + +import static java.util.jar.Attributes.Name.MANIFEST_VERSION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PluginJarLoaderTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public LogTester logs = new LogTester(); + + private ServerFileSystem fs = mock(ServerFileSystem.class); + private Set blacklisted = new HashSet<>(); + private SonarRuntime runtime = mock(SonarRuntime.class); + private PluginJarLoader underTest = new PluginJarLoader(fs, runtime, blacklisted); + + @Before + public void setUp() throws IOException { + when(runtime.getApiVersion()).thenReturn(org.sonar.api.utils.Version.parse("5.2")); + when(fs.getDeployedPluginsDir()).thenReturn(temp.newFolder("deployed")); + when(fs.getDownloadedPluginsDir()).thenReturn(temp.newFolder("downloaded")); + when(fs.getHomeDir()).thenReturn(temp.newFolder("home")); + when(fs.getInstalledExternalPluginsDir()).thenReturn(temp.newFolder("external")); + when(fs.getInstalledBundledPluginsDir()).thenReturn(temp.newFolder("bundled")); + when(fs.getTempDir()).thenReturn(temp.newFolder("temp")); + } + + @Test + public void load_installed_bundled_and_external_plugins() throws Exception { + copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + copyTestPluginTo("test-extend-plugin", fs.getInstalledBundledPluginsDir()); + + Collection loadedPlugins = underTest.loadPlugins(); + + assertThat(loadedPlugins).extracting(PluginInfo::getKey).containsOnly("testbase", "testextend"); + } + + @Test + public void dont_fail_if_directories_dont_exist() { + FileUtils.deleteQuietly(fs.getInstalledExternalPluginsDir()); + FileUtils.deleteQuietly(fs.getInstalledBundledPluginsDir()); + FileUtils.deleteQuietly(fs.getDownloadedPluginsDir()); + Collection loadedPlugins = underTest.loadPlugins(); + assertThat(loadedPlugins).extracting(PluginInfo::getKey).isEmpty(); + } + + @Test + public void update_downloaded_plugin() throws IOException { + File jar = createJar(fs.getDownloadedPluginsDir(), "plugin1", "main", null, "2.0"); + createJar(fs.getInstalledExternalPluginsDir(), "plugin1", "main", null, "1.0"); + + underTest.loadPlugins(); + + assertThat(logs.logs()).contains("Plugin plugin1 [plugin1] updated to version 2.0"); + assertThat(Files.list(fs.getInstalledExternalPluginsDir().toPath())).extracting(Path::getFileName).containsOnly(jar.toPath().getFileName()); + } + + @Test + public void move_downloaded_plugins_to_external() throws Exception { + copyTestPluginTo("test-base-plugin", fs.getDownloadedPluginsDir()); + copyTestPluginTo("test-extend-plugin", fs.getInstalledExternalPluginsDir()); + assertThat(Files.list(fs.getInstalledExternalPluginsDir().toPath())).hasSize(1); + + Collection loadedPlugins = underTest.loadPlugins(); + + assertThat(loadedPlugins).extracting(PluginInfo::getKey).containsOnly("testbase", "testextend"); + assertThat(fs.getDownloadedPluginsDir()).isEmptyDirectory(); + assertThat(Files.list(fs.getInstalledExternalPluginsDir().toPath())).hasSize(2); + } + + @Test + public void no_plugins_at_startup() { + assertThat(underTest.loadPlugins()).isEmpty(); + } + + @Test + public void test_plugin_requirements_at_startup() throws Exception { + copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); + + assertThat(underTest.loadPlugins()).extracting(PluginInfo::getKey).containsOnly("testbase", "testrequire"); + } + + @Test + public void plugin_is_ignored_if_required_plugin_is_missing_at_startup() throws Exception { + copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); + + // plugin is not installed as test-base-plugin is missing + assertThat(underTest.loadPlugins()).isEmpty(); + assertThat(logs.logs()).contains("Plugin Test Require Plugin [testrequire] is ignored because the required plugin [testbase] is not installed"); + } + + @Test + public void install_plugin_and_its_extension_plugins_at_startup() throws Exception { + copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + copyTestPluginTo("test-extend-plugin", fs.getInstalledExternalPluginsDir()); + + // both plugins are installed + assertThat(underTest.loadPlugins()).extracting(PluginInfo::getKey).containsOnly("testbase", "testextend"); + } + + /** + * Some plugins can only extend the classloader of base plugin, without declaring new extensions. + */ + @Test + public void plugin_is_compatible_if_no_entry_point_class_but_extend_other_plugin() throws IOException { + createJar(fs.getInstalledExternalPluginsDir(), "base", "org.bar.Bar", null); + createJar(fs.getInstalledExternalPluginsDir(), "foo", null, "base"); + + assertThat(underTest.loadPlugins()).extracting(PluginInfo::getKey).containsOnly("base", "foo"); + } + + @Test + public void extension_plugin_is_ignored_if_base_plugin_is_missing_at_startup() throws Exception { + copyTestPluginTo("test-extend-plugin", fs.getInstalledExternalPluginsDir()); + + assertThat(underTest.loadPlugins()).isEmpty(); + assertThat(logs.logs()).contains("Plugin Test Extend Plugin [testextend] is ignored because its base plugin [testbase] is not installed"); + } + + @Test + public void plugin_is_ignored_if_required_plugin_is_too_old_at_startup() throws Exception { + copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + copyTestPluginTo("test-requirenew-plugin", fs.getInstalledExternalPluginsDir()); + + // the plugin "requirenew" is not installed as it requires base 0.2+ to be installed. + assertThat(underTest.loadPlugins()).extracting(PluginInfo::getKey).containsOnly("testbase"); + assertThat(logs.logs()).contains("Plugin Test Require New Plugin [testrequire] is ignored because the version 0.2 of required plugin [testbase] is not installed"); + } + + @Test + public void blacklisted_plugin_is_automatically_deleted() throws Exception { + blacklisted.add("testbase"); + blacklisted.add("issuesreport"); + + File jar = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + + Collection loadedPlugins = underTest.loadPlugins(); + + // plugin is not installed and file is deleted + assertThat(loadedPlugins).isEmpty(); + assertThat(jar).doesNotExist(); + } + + @Test + public void warn_if_plugin_has_no_entry_point_class() throws IOException { + createJar(fs.getInstalledExternalPluginsDir(), "test", null, null); + assertThat(underTest.loadPlugins()).isEmpty(); + assertThat(logs.logs()).contains("Plugin test [test] is ignored because entry point class is not defined"); + } + + @Test + public void fail_if_external_plugin_has_same_key_has_bundled_plugin() throws IOException { + File jar = createJar(fs.getInstalledExternalPluginsDir(), "plugin1", "main", null); + createJar(fs.getInstalledBundledPluginsDir(), "plugin1", "main", null); + + String dir = getDirName(fs.getInstalledExternalPluginsDir()); + expectedException.expectMessage("Found a plugin 'plugin1' in the directory " + dir + " with the same key [plugin1] as a bundled plugin 'plugin1'. " + + "Please remove " + jar.getName()); + expectedException.expect(MessageException.class); + underTest.loadPlugins(); + } + + @Test + public void fail_if_downloaded_plugin_has_same_key_has_bundled() throws IOException { + File downloaded = createJar(fs.getDownloadedPluginsDir(), "plugin1", "main", null); + createJar(fs.getInstalledBundledPluginsDir(), "plugin1", "main", null); + String dir = getDirName(fs.getDownloadedPluginsDir()); + expectedException.expectMessage("Fail to update plugin: plugin1. Bundled plugin with same key already exists: plugin1. " + + "Move or delete plugin from " + dir + " directory"); + expectedException.expect(MessageException.class); + underTest.loadPlugins(); + } + + @Test + public void fail_if_external_plugins_have_same_key() throws IOException { + File jar1 = createJar(fs.getInstalledExternalPluginsDir(), "plugin1", "main", null); + File jar2 = createJar(fs.getInstalledExternalPluginsDir(), "plugin1", "main", null); + + String dir = getDirName(fs.getInstalledExternalPluginsDir()); + expectedException.expectMessage("Found two versions of the plugin 'plugin1' [plugin1] in the directory " + dir + ". Please remove "); + expectedException.expectMessage(jar2.getName()); + expectedException.expectMessage(jar1.getName()); + expectedException.expect(MessageException.class); + underTest.loadPlugins(); + } + + @Test + public void fail_if_bundled_plugins_have_same_key() throws IOException { + File jar1 = createJar(fs.getInstalledBundledPluginsDir(), "plugin1", "main", null); + File jar2 = createJar(fs.getInstalledBundledPluginsDir(), "plugin1", "main", null); + String dir = getDirName(fs.getInstalledBundledPluginsDir()); + expectedException.expectMessage("Found two versions of the plugin plugin1 [plugin1] in the directory " + dir + ". Please remove one of "); + expectedException.expectMessage(jar1.getName()); + expectedException.expectMessage(jar2.getName()); + expectedException.expect(MessageException.class); + underTest.loadPlugins(); + } + + @Test + public void fail_when_sqale_plugin_is_installed() throws Exception { + copyTestPluginTo("fake-sqale-plugin", fs.getInstalledExternalPluginsDir()); + + expectedException.expect(MessageException.class); + expectedException.expectMessage("Plugin 'sqale' is no longer compatible with this version of SonarQube"); + underTest.loadPlugins(); + } + + @Test + public void fail_when_report_is_installed() throws Exception { + copyTestPluginTo("fake-report-plugin", fs.getInstalledExternalPluginsDir()); + + expectedException.expect(MessageException.class); + expectedException.expectMessage("Plugin 'report' is no longer compatible with this version of SonarQube"); + underTest.loadPlugins(); + } + + @Test + public void fail_when_views_is_installed() throws Exception { + copyTestPluginTo("fake-views-plugin", fs.getInstalledExternalPluginsDir()); + + expectedException.expect(MessageException.class); + expectedException.expectMessage("Plugin 'views' is no longer compatible with this version of SonarQube"); + underTest.loadPlugins(); + } + + @Test + public void fail_if_plugin_does_not_support_sq_version() throws Exception { + when(runtime.getApiVersion()).thenReturn(org.sonar.api.utils.Version.parse("1.0")); + copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + + expectedException.expectMessage("Plugin Base Plugin [testbase] requires at least SonarQube 4.5.4"); + underTest.loadPlugins(); + } + + private static File copyTestPluginTo(String testPluginName, File toDir) throws IOException { + File jar = TestProjectUtils.jarOf(testPluginName); + // file is copied because it's supposed to be moved by the test + FileUtils.copyFileToDirectory(jar, toDir); + return new File(toDir, jar.getName()); + } + + private static String getDirName(File dir) { + Path path = dir.toPath(); + return new File(path.getName(path.getNameCount() - 2).toString(), path.getName(path.getNameCount() - 1).toString()).toString(); + } + + private static File createJar(File dir, String key, @Nullable String mainClass, @Nullable String basePlugin) throws IOException { + return createJar(dir, key, mainClass, basePlugin, null); + } + + private static File createJar(File dir, String key, @Nullable String mainClass, @Nullable String basePlugin, @Nullable String version) throws IOException { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue(PluginManifest.KEY, key); + manifest.getMainAttributes().putValue(PluginManifest.NAME, key); + if (version != null) { + manifest.getMainAttributes().putValue(PluginManifest.VERSION, version); + } + if (mainClass != null) { + manifest.getMainAttributes().putValue(PluginManifest.MAIN_CLASS, mainClass); + } + if (basePlugin != null) { + manifest.getMainAttributes().putValue(PluginManifest.BASE_PLUGIN, basePlugin); + } + manifest.getMainAttributes().putValue(MANIFEST_VERSION.toString(), "1.0"); + File jarFile = File.createTempFile(key, ".jar", dir); + try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(jarFile), manifest)) { + // nothing else to add + } + return jarFile; + } +} diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java index 0144ead6eb7..5e92e9087e1 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java @@ -21,72 +21,119 @@ package org.sonar.server.plugins; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; +import org.sonar.api.Plugin; +import org.sonar.api.utils.log.LogTester; +import org.sonar.core.platform.PluginInfo; import org.sonar.server.platform.ServerFileSystem; +import org.sonar.updatecenter.common.Version; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import static org.sonar.server.plugins.PluginType.BUNDLED; +import static org.sonar.server.plugins.PluginType.EXTERNAL; public class PluginUninstallerTest { @Rule public TemporaryFolder testFolder = new TemporaryFolder(); - @Rule public ExpectedException exception = ExpectedException.none(); + @Rule + public LogTester logs = new LogTester(); private File uninstallDir; - private PluginUninstaller underTest; - private ServerPluginRepository serverPluginRepository; - private ServerFileSystem fs; + private ServerFileSystem fs = mock(ServerFileSystem.class); + private ServerPluginRepository serverPluginRepository = new ServerPluginRepository(); + private PluginUninstaller underTest = new PluginUninstaller(fs, serverPluginRepository); @Before public void setUp() throws IOException { - serverPluginRepository = mock(ServerPluginRepository.class); uninstallDir = testFolder.newFolder("uninstall"); - fs = mock(ServerFileSystem.class); when(fs.getUninstalledPluginsDir()).thenReturn(uninstallDir); - underTest = new PluginUninstaller(serverPluginRepository, fs); + when(fs.getInstalledExternalPluginsDir()).thenReturn(testFolder.newFolder("external")); } @Test - public void uninstall() { - when(serverPluginRepository.hasPlugin("plugin")).thenReturn(true); - underTest.uninstall("plugin"); - verify(serverPluginRepository).uninstall("plugin", uninstallDir); + public void create_uninstall_dir() { + File dir = new File(testFolder.getRoot(), "dir"); + when(fs.getUninstalledPluginsDir()).thenReturn(dir); + + assertThat(dir).doesNotExist(); + underTest.start(); + assertThat(dir).isDirectory(); } @Test - public void fail_uninstall_if_plugin_not_installed() { - when(serverPluginRepository.hasPlugin("plugin")).thenReturn(false); - exception.expect(IllegalArgumentException.class); - exception.expectMessage("Plugin [plugin] is not installed"); - underTest.uninstall("plugin"); - verifyZeroInteractions(serverPluginRepository); + public void fail_uninstall_if_plugin_doesnt_exist() { + underTest.start(); + assertThatThrownBy(() -> underTest.uninstall("plugin")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Plugin [plugin] is not installed"); } @Test - public void create_uninstall_dir() { - File dir = new File(testFolder.getRoot(), "dir"); - when(fs.getUninstalledPluginsDir()).thenReturn(dir); - underTest = new PluginUninstaller(serverPluginRepository, fs); + public void fail_uninstall_if_plugin_is_bundled() { underTest.start(); - assertThat(dir).isDirectory(); + serverPluginRepository.addPlugin(newPlugin("plugin", BUNDLED, "plugin.jar")); + assertThatThrownBy(() -> underTest.uninstall("plugin")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Plugin [plugin] is not installed"); + } + + @Test + public void uninstall() throws Exception { + File installedJar = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + serverPluginRepository.addPlugin(newPlugin("testbase", EXTERNAL, installedJar.getName())); + + underTest.start(); + assertThat(installedJar).exists(); + + underTest.uninstall("testbase"); + + assertThat(installedJar).doesNotExist(); + assertThat(uninstallDir.list()).containsOnly(installedJar.getName()); } @Test - public void cancel() { + public void uninstall_ignores_non_existing_files() { + underTest.start(); + serverPluginRepository.addPlugin(newPlugin("test", EXTERNAL, "nonexisting.jar")); + underTest.uninstall("test"); + assertThat(uninstallDir).isEmptyDirectory(); + assertThat(logs.logs()).contains("Plugin already uninstalled: test [test]"); + } + + @Test + public void uninstall_dependents() throws IOException { + File baseJar = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); + File requirejar = copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); + + ServerPlugin base = newPlugin("test-base-plugin", EXTERNAL, baseJar.getName()); + ServerPlugin extension = newPlugin("test-require-plugin", EXTERNAL, requirejar.getName(), new PluginInfo.RequiredPlugin("test-base-plugin", Version.create("1.0"))); + + serverPluginRepository.addPlugins(Arrays.asList(base, extension)); + + underTest.start(); + underTest.uninstall("test-base-plugin"); + assertThat(Files.list(uninstallDir.toPath())).extracting(p -> p.getFileName().toString()).containsOnly(baseJar.getName(), requirejar.getName()); + assertThat(fs.getInstalledExternalPluginsDir()).isEmptyDirectory(); + } + + @Test + public void cancel() throws IOException { + File file = copyTestPluginTo("test-base-plugin", uninstallDir); + assertThat(Files.list(uninstallDir.toPath())).extracting(p -> p.getFileName().toString()).containsOnly(file.getName()); underTest.cancelUninstalls(); - verify(serverPluginRepository).cancelUninstalls(uninstallDir); - verifyNoMoreInteractions(serverPluginRepository); } @Test @@ -96,6 +143,30 @@ public class PluginUninstallerTest { assertThat(underTest.getUninstalledPlugins()).extracting("key").containsOnly("testbase"); } + private static ServerPlugin newPlugin(String key, PluginType type, String jarFile, PluginInfo.RequiredPlugin requiredPlugin) { + ServerPluginInfo pluginInfo = newPluginInfo(key, type, jarFile); + when(pluginInfo.getRequiredPlugins()).thenReturn(Collections.singleton(requiredPlugin)); + return newPlugin(pluginInfo); + } + + private static ServerPlugin newPlugin(String key, PluginType type, String jarFile) { + return newPlugin(newPluginInfo(key, type, jarFile)); + } + + private static ServerPluginInfo newPluginInfo(String key, PluginType type, String jarFile) { + ServerPluginInfo pluginInfo = mock(ServerPluginInfo.class); + when(pluginInfo.getKey()).thenReturn(key); + when(pluginInfo.getName()).thenReturn(key); + when(pluginInfo.getType()).thenReturn(type); + when(pluginInfo.getNonNullJarFile()).thenReturn(new File(jarFile)); + return pluginInfo; + } + + private static ServerPlugin newPlugin(ServerPluginInfo pluginInfo) { + return new ServerPlugin(pluginInfo, pluginInfo.getType(), mock(Plugin.class), + mock(PluginFilesAndMd5.FileAndMd5.class), mock(PluginFilesAndMd5.FileAndMd5.class), mock(ClassLoader.class)); + } + private File copyTestPluginTo(String testPluginName, File toDir) throws IOException { File jar = TestProjectUtils.jarOf(testPluginName); // file is copied because it's supposed to be moved by the test diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginInfoTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginInfoTest.java new file mode 100644 index 00000000000..08162441799 --- /dev/null +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginInfoTest.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.plugins.PluginType.BUNDLED; +import static org.sonar.server.plugins.PluginType.EXTERNAL; + +public class ServerPluginInfoTest { + @Test + public void equals_returns_false_with_different_types() { + ServerPluginInfo info1 = new ServerPluginInfo("key1").setType(EXTERNAL); + ServerPluginInfo info2 = new ServerPluginInfo("key1").setType(PluginType.BUNDLED); + ServerPluginInfo info3 = new ServerPluginInfo("key1").setType(EXTERNAL); + + assertThat(info1).isNotEqualTo(info2) + .isEqualTo(info3) + .hasSameHashCodeAs(info3.hashCode()); + assertThat(info1.hashCode()).isNotEqualTo(info2.hashCode()); + } + + @Test + public void set_and_get_type() { + ServerPluginInfo info = new ServerPluginInfo("key1").setType(EXTERNAL); + assertThat(info.getType()).isEqualTo(EXTERNAL); + + info.setType(BUNDLED); + assertThat(info.getType()).isEqualTo(BUNDLED); + } +} diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java index 68e60276958..78a9473b5cb 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java @@ -29,7 +29,6 @@ import org.sonar.server.platform.ServerFileSystem; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class ServerPluginJarExploderTest { @@ -38,8 +37,7 @@ public class ServerPluginJarExploderTest { public TemporaryFolder temp = new TemporaryFolder(); private ServerFileSystem fs = mock(ServerFileSystem.class); - private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class); - private ServerPluginJarExploder underTest = new ServerPluginJarExploder(fs, pluginFileSystem); + private ServerPluginJarExploder underTest = new ServerPluginJarExploder(fs); @Test public void copy_all_classloader_files_to_dedicated_directory() throws Exception { @@ -62,6 +60,5 @@ public class ServerPluginJarExploderTest { assertThat(lib.getCanonicalPath()).startsWith(pluginDeployDir.getCanonicalPath()); } File targetJar = new File(fs.getDeployedPluginsDir(), "testlibs/test-libs-plugin-0.1-SNAPSHOT.jar"); - verify(pluginFileSystem).addInstalledPlugin(info, targetJar); } } diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginManagerTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginManagerTest.java new file mode 100644 index 00000000000..bd476600444 --- /dev/null +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginManagerTest.java @@ -0,0 +1,104 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.plugins; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.Plugin; +import org.sonar.api.utils.log.LogTester; +import org.sonar.core.platform.ExplodedPlugin; +import org.sonar.core.platform.PluginClassLoader; +import org.sonar.core.platform.PluginJarExploder; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.server.plugins.PluginType.EXTERNAL; + +public class ServerPluginManagerTest { + + @Rule + public LogTester logs = new LogTester(); + + private PluginClassLoader pluginClassLoader = mock(PluginClassLoader.class); + private PluginJarExploder jarExploder = mock(PluginJarExploder.class); + private PluginJarLoader jarLoader = mock(PluginJarLoader.class); + private PluginCompressor pluginCompressor = mock(PluginCompressor.class); + private ServerPluginRepository pluginRepository = new ServerPluginRepository(); + private ServerPluginManager underTest = new ServerPluginManager(pluginClassLoader, jarExploder, jarLoader, pluginCompressor, pluginRepository); + + @After + public void tearDown() { + underTest.stop(); + } + + @Test + public void load_plugins() { + ServerPluginInfo p1 = newPluginInfo("p1"); + ServerPluginInfo p2 = newPluginInfo("p2"); + when(jarLoader.loadPlugins()).thenReturn(Arrays.asList(p1, p2)); + when(jarExploder.explode(p1)).thenReturn(new ExplodedPlugin(p1, "p1", new File("p1Exploded.jar"), Collections.singletonList(new File("libP1.jar")))); + when(jarExploder.explode(p2)).thenReturn(new ExplodedPlugin(p2, "p2", new File("p2Exploded.jar"), Collections.singletonList(new File("libP2.jar")))); + + Map instances = ImmutableMap.of("p1", mock(Plugin.class), "p2", mock(Plugin.class)); + when(pluginClassLoader.load(anyList())).thenReturn(instances); + PluginFilesAndMd5 p1Files = newPluginFilesAndMd5("p1"); + PluginFilesAndMd5 p2Files = newPluginFilesAndMd5("p2"); + + when(pluginCompressor.compress("p1", new File("p1.jar"), new File("p1Exploded.jar"))).thenReturn(p1Files); + when(pluginCompressor.compress("p2", new File("p2.jar"), new File("p2Exploded.jar"))).thenReturn(p2Files); + + underTest.start(); + + assertThat(pluginRepository.getPlugins()) + .extracting(ServerPlugin::getPluginInfo, ServerPlugin::getCompressed, ServerPlugin::getJar, ServerPlugin::getInstance) + .containsOnly(tuple(p1, p1Files.getCompressedJar(), p1Files.getLoadedJar(), instances.get("p1")), + tuple(p2, p2Files.getCompressedJar(), p2Files.getLoadedJar(), instances.get("p2"))); + } + + private static ServerPluginInfo newPluginInfo(String key) { + ServerPluginInfo pluginInfo = mock(ServerPluginInfo.class); + when(pluginInfo.getKey()).thenReturn(key); + when(pluginInfo.getType()).thenReturn(EXTERNAL); + when(pluginInfo.getNonNullJarFile()).thenReturn(new File(key + ".jar")); + return pluginInfo; + } + + private static PluginFilesAndMd5 newPluginFilesAndMd5(String name) { + FileAndMd5 jar = mock(FileAndMd5.class); + when(jar.getFile()).thenReturn(new File(name)); + when(jar.getMd5()).thenReturn(name + "-md5"); + + FileAndMd5 compressed = mock(FileAndMd5.class); + when(compressed.getFile()).thenReturn(new File(name + "-compressed")); + when(compressed.getMd5()).thenReturn(name + "-compressed-md5"); + + return new PluginFilesAndMd5(jar, compressed); + } +} diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java index ff8b394c7e9..1b834913f85 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java @@ -19,422 +19,66 @@ */ package org.sonar.server.plugins; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import java.io.File; -import java.io.IOException; +import java.util.Arrays; import java.util.Collections; -import java.util.Map; -import org.apache.commons.io.FileUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; +import org.junit.Assert; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; -import org.sonar.api.SonarRuntime; -import org.sonar.api.utils.MessageException; -import org.sonar.api.utils.log.LogTester; +import org.sonar.api.Plugin; import org.sonar.core.platform.PluginInfo; -import org.sonar.core.platform.PluginLoader; -import org.sonar.server.platform.ServerFileSystem; -import org.sonar.updatecenter.common.Version; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.sonar.server.plugins.PluginType.EXTERNAL; public class ServerPluginRepositoryTest { - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - @Rule - public TemporaryFolder temp = new TemporaryFolder(); - - @Rule - public LogTester logs = new LogTester(); - - private SonarRuntime runtime = mock(SonarRuntime.class); - private ServerFileSystem fs = mock(ServerFileSystem.class, Mockito.RETURNS_DEEP_STUBS); - private PluginLoader pluginLoader = mock(PluginLoader.class); - private ServerPluginRepository underTest = new ServerPluginRepository(runtime, fs, pluginLoader); - - @Before - public void setUp() throws IOException { - when(fs.getDeployedPluginsDir()).thenReturn(temp.newFolder()); - when(fs.getDownloadedPluginsDir()).thenReturn(temp.newFolder()); - when(fs.getHomeDir()).thenReturn(temp.newFolder()); - when(fs.getInstalledExternalPluginsDir()).thenReturn(temp.newFolder()); - when(fs.getInstalledBundledPluginsDir()).thenReturn(temp.newFolder()); - when(fs.getTempDir()).thenReturn(temp.newFolder()); - when(runtime.getApiVersion()).thenReturn(org.sonar.api.utils.Version.parse("5.2")); - } - - @After - public void tearDown() { - underTest.stop(); - } + private ServerPluginRepository repository = new ServerPluginRepository(); @Test - public void standard_startup_loads_installed_bundled_and_external_plugins() throws Exception { - copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - copyTestPluginTo("test-extend-plugin", fs.getInstalledBundledPluginsDir()); + public void get_plugin_data() { + ServerPlugin plugin1 = newPlugin("plugin1"); + ServerPlugin plugin2 = newPlugin("plugin2"); - underTest.start(); - - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase", "testextend"); - } - - @Test - public void no_plugins_at_all_on_startup() { - underTest.start(); - - assertThat(underTest.getPluginInfos()).isEmpty(); - assertThat(underTest.getPluginInfosByKeys()).isEmpty(); - assertThat(underTest.hasPlugin("testbase")).isFalse(); - } - - @Test - public void fail_if_multiple_jars_for_same_installed_external_plugin_on_startup() throws Exception { - copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - copyTestPluginTo("test-base-plugin-v2", fs.getInstalledExternalPluginsDir()); + repository.addPlugins(Collections.singletonList(plugin1)); + repository.addPlugin(plugin2); + assertThat(repository.getPluginInfos()).containsOnly(plugin1.getPluginInfo(), plugin2.getPluginInfo()); + assertThat(repository.getPluginInstance("plugin1")).isEqualTo(plugin1.getInstance()); + assertThat(repository.getPluginInstances()).containsOnly(plugin1.getInstance(), plugin2.getInstance()); + assertThat(repository.getPlugins()).containsOnly(plugin1, plugin2); + assertThat(repository.getPlugin("plugin2")).isEqualTo(plugin2); + assertThat(repository.findPlugin("plugin2")).contains(plugin2); + assertThat(repository.hasPlugin("plugin2")).isTrue(); - try { - underTest.start(); - fail(); - } catch (MessageException e) { - assertThat(e) - .hasMessageStartingWith("Found two versions of the plugin Base Plugin [testbase] in the directory extensions/plugins. Please remove one of ") - // order is not guaranteed, so assertion is split - .hasMessageContaining("test-base-plugin-0.1-SNAPSHOT.jar") - .hasMessageContaining("test-base-plugin-0.2-SNAPSHOT.jar"); - } + assertThat(repository.findPlugin("nonexisting")).isEmpty(); + assertThat(repository.hasPlugin("nonexisting")).isFalse(); } @Test - public void fail_if_multiple_jars_for_same_installed_bundled_plugin_on_startup() throws Exception { - copyTestPluginTo("test-base-plugin", fs.getInstalledBundledPluginsDir()); - copyTestPluginTo("test-base-plugin-v2", fs.getInstalledBundledPluginsDir()); + public void fail_getPluginInstance_if_plugin_doesnt_exist() { + ServerPlugin plugin1 = newPlugin("plugin1"); + ServerPlugin plugin2 = newPlugin("plugin2"); - try { - underTest.start(); - fail(); - } catch (MessageException e) { - assertThat(e) - .hasMessageStartingWith("Found two versions of the plugin Base Plugin [testbase] in the directory lib/extensions. Please remove one of ") - // order is not guaranteed, so assertion is split - .hasMessageContaining("test-base-plugin-0.1-SNAPSHOT.jar") - .hasMessageContaining("test-base-plugin-0.2-SNAPSHOT.jar"); - } + repository.addPlugins(Arrays.asList(plugin1, plugin2)); + Assert.assertThrows("asd", IllegalArgumentException.class, () -> repository.getPluginInstance("plugin3")); } @Test - public void fail_if_multiple_jars_for_same_installed_external_bundled_plugin_on_startup() throws Exception { - copyTestPluginTo("test-base-plugin", fs.getInstalledBundledPluginsDir()); - copyTestPluginTo("test-base-plugin-v2", fs.getInstalledExternalPluginsDir()); - - try { - underTest.start(); - fail(); - } catch (MessageException e) { - assertThat(e) - .hasMessageStartingWith( - "Found two versions of the plugin Base Plugin [testbase] in different directories lib/extensions and extension/plugins. Please remove the one from extension/plugins: ") - .hasMessageContaining("test-base-plugin-0.2-SNAPSHOT.jar"); - } - } - - @Test - public void install_downloaded_plugins_on_startup() throws Exception { - File downloadedJar = copyTestPluginTo("test-base-plugin", fs.getDownloadedPluginsDir()); - - underTest.start(); - - // plugin is moved to extensions/plugins then loaded - assertThat(downloadedJar).doesNotExist(); - assertThat(new File(fs.getInstalledExternalPluginsDir(), downloadedJar.getName())).isFile().exists(); - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); - } - - @Test - public void downloaded_file_overrides_existing_installed_file_on_startup() throws Exception { - File installedV1 = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - File downloadedV2 = copyTestPluginTo("test-base-plugin-v2", fs.getDownloadedPluginsDir()); - - underTest.start(); - - // plugin is moved to extensions/plugins and replaces v1 - assertThat(downloadedV2).doesNotExist(); - assertThat(installedV1).doesNotExist(); - assertThat(new File(fs.getInstalledExternalPluginsDir(), downloadedV2.getName())).exists(); - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); - assertThat(underTest.getPluginInfo("testbase").getVersion()).isEqualTo(Version.create("0.2-SNAPSHOT")); - } - - @Test - public void downloaded_file_does_not_override_existing_bundled_file_on_startup() throws Exception { - File installedV1 = copyTestPluginTo("test-base-plugin", fs.getInstalledBundledPluginsDir()); - File downloadedV2 = copyTestPluginTo("test-base-plugin-v2", fs.getDownloadedPluginsDir()); - - assertThatThrownBy(() -> underTest.start()) - .isInstanceOf(MessageException.class) - .hasMessage("Fail to update plugin: test-base-plugin-0.2-SNAPSHOT.jar. Bundled plugin with same key already exists: testbase. " - + "Move or delete plugin from extensions/downloads directory"); + public void fail_getPluginInfo_if_plugin_doesnt_exist() { + ServerPlugin plugin1 = newPlugin("plugin1"); + ServerPlugin plugin2 = newPlugin("plugin2"); - // downloaded plugin stays in origin location - assertThat(downloadedV2).exists(); - // installed plugin has not been deleted - assertThat(installedV1).exists(); - assertThat(new File(fs.getInstalledExternalPluginsDir(), downloadedV2.getName())).doesNotExist(); - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); + repository.addPlugins(Arrays.asList(plugin1, plugin2)); + Assert.assertThrows("asd", IllegalArgumentException.class, () -> repository.getPluginInfo("plugin3")); } - @Test - public void blacklisted_plugin_is_automatically_uninstalled_on_startup() throws Exception { - underTest.setBlacklistedPluginKeys(ImmutableSet.of("testbase", "issuesreport")); - File jar = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - - underTest.start(); - - // plugin is not installed and file is deleted - assertThat(underTest.getPluginInfos()).isEmpty(); - assertThat(jar).doesNotExist(); - } - - @Test - public void test_plugin_requirements_at_startup() throws Exception { - copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); - - underTest.start(); - - // both plugins are installed - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase", "testrequire"); - } - - @Test - public void plugin_is_ignored_if_required_plugin_is_missing_at_startup() throws Exception { - copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); - - underTest.start(); - - // plugin is not installed as test-base-plugin is missing - assertThat(underTest.getPluginInfosByKeys()).isEmpty(); - } - - @Test - public void plugin_is_ignored_if_required_plugin_is_too_old_at_startup() throws Exception { - copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - copyTestPluginTo("test-requirenew-plugin", fs.getInstalledExternalPluginsDir()); - - underTest.start(); - - assertThat(logs.logs()).contains("Plugin Test Require New Plugin [testrequire] is ignored because the version 0.2 of required plugin [testbase] is not installed"); - // the plugin "requirenew" is not installed as it requires base 0.2+ to be installed. - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); - } - - @Test - public void fail_if_plugin_does_not_support_sq_version() throws Exception { - when(runtime.getApiVersion()).thenReturn(org.sonar.api.utils.Version.parse("1.0")); - copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - - try { - underTest.start(); - fail(); - } catch (MessageException e) { - assertThat(e).hasMessage("Plugin Base Plugin [testbase] requires at least SonarQube 4.5.4"); - } - } - - @Test - public void uninstall() throws Exception { - File installedJar = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - File uninstallDir = temp.newFolder("uninstallDir"); - - underTest.start(); - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); - underTest.uninstall("testbase", uninstallDir); - - assertThat(installedJar).doesNotExist(); - // still up. Will be dropped after next startup - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); - assertThat(uninstallDir.list()).containsOnly(installedJar.getName()); - } - - @Test - public void uninstall_dependents() throws Exception { - File base = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - File extension = copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); - File uninstallDir = temp.newFolder("uninstallDir"); - - underTest.start(); - assertThat(underTest.getPluginInfos()).hasSize(2); - underTest.uninstall("testbase", uninstallDir); - assertThat(base).doesNotExist(); - assertThat(extension).doesNotExist(); - assertThat(uninstallDir.list()).containsOnly(base.getName(), extension.getName()); - } - - @Test - public void dont_uninstall_non_existing_dependents() throws IOException { - File base = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - File extension = copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); - File uninstallDir = temp.newFolder("uninstallDir"); - - underTest.start(); - assertThat(underTest.getPluginInfos()).hasSize(2); - underTest.uninstall("testrequire", uninstallDir); - assertThat(underTest.getPluginInfos()).hasSize(2); - - underTest.uninstall("testbase", uninstallDir); - assertThat(base).doesNotExist(); - assertThat(extension).doesNotExist(); - assertThat(uninstallDir.list()).containsOnly(base.getName(), extension.getName()); - } - - @Test - public void dont_uninstall_non_existing_files() throws IOException { - File base = copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - File extension = copyTestPluginTo("test-require-plugin", fs.getInstalledExternalPluginsDir()); - File uninstallDir = temp.newFolder("uninstallDir"); - - underTest.start(); - assertThat(underTest.getPluginInfos()).hasSize(2); - underTest.uninstall("testbase", uninstallDir); - assertThat(underTest.getPluginInfos()).hasSize(2); - - underTest.uninstall("testbase", uninstallDir); - assertThat(base).doesNotExist(); - assertThat(extension).doesNotExist(); - assertThat(uninstallDir.list()).containsOnly(base.getName(), extension.getName()); - } - - @Test - public void install_plugin_and_its_extension_plugins_at_startup() throws Exception { - copyTestPluginTo("test-base-plugin", fs.getInstalledExternalPluginsDir()); - copyTestPluginTo("test-extend-plugin", fs.getInstalledExternalPluginsDir()); - - underTest.start(); - - // both plugins are installed - assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase", "testextend"); - } - - @Test - public void extension_plugin_is_ignored_if_base_plugin_is_missing_at_startup() throws Exception { - copyTestPluginTo("test-extend-plugin", fs.getInstalledExternalPluginsDir()); - - underTest.start(); - - // plugin is not installed as its base plugin is not installed - assertThat(underTest.getPluginInfos()).isEmpty(); - } - - @Test - public void fail_to_get_missing_plugins() { - underTest.start(); - try { - underTest.getPluginInfo("unknown"); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessage("Plugin [unknown] does not exist"); - } - - try { - underTest.getPluginInstance("unknown"); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessage("Plugin [unknown] does not exist"); - } - } - - @Test - public void plugin_is_incompatible_if_no_entry_point_class() { - PluginInfo plugin = new PluginInfo("foo").setName("Foo"); - assertThat(ServerPluginRepository.isCompatible(plugin, runtime, Collections.emptyMap())).isFalse(); - assertThat(logs.logs()).contains("Plugin Foo [foo] is ignored because entry point class is not defined"); - } - - @Test - public void fail_when_views_is_installed() throws Exception { - copyTestPluginTo("fake-views-plugin", fs.getInstalledExternalPluginsDir()); - - expectedException.expect(MessageException.class); - expectedException.expectMessage("Plugin 'views' is no longer compatible with this version of SonarQube"); - underTest.start(); - } - - @Test - public void fail_when_sqale_plugin_is_installed() throws Exception { - copyTestPluginTo("fake-sqale-plugin", fs.getInstalledExternalPluginsDir()); - - expectedException.expect(MessageException.class); - expectedException.expectMessage("Plugin 'sqale' is no longer compatible with this version of SonarQube"); - underTest.start(); - } - - @Test - public void fail_when_report_is_installed() throws Exception { - copyTestPluginTo("fake-report-plugin", fs.getInstalledExternalPluginsDir()); - - expectedException.expect(MessageException.class); - expectedException.expectMessage("Plugin 'report' is no longer compatible with this version of SonarQube"); - underTest.start(); - } - - /** - * Some plugins can only extend the classloader of base plugin, without declaring new extensions. - */ - @Test - public void plugin_is_compatible_if_no_entry_point_class_but_extend_other_plugin() { - PluginInfo basePlugin = new PluginInfo("base").setMainClass("org.bar.Bar"); - PluginInfo plugin = new PluginInfo("foo").setBasePlugin("base"); - Map plugins = ImmutableMap.of("base", basePlugin, "foo", plugin); - - assertThat(ServerPluginRepository.isCompatible(plugin, runtime, plugins)).isTrue(); - } - - @Test - public void getPluginInstance_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.getPluginInstance("foo"); - } - - @Test - public void getPluginInfo_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.getPluginInfo("foo"); - } - - @Test - public void hasPlugin_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.hasPlugin("foo"); - } - - @Test - public void getPluginInfos_throws_ISE_if_repo_is_not_started() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("not started yet"); - - underTest.getPluginInfos(); + private PluginInfo newPluginInfo(String key) { + PluginInfo pluginInfo = mock(PluginInfo.class); + when(pluginInfo.getKey()).thenReturn(key); + return pluginInfo; } - private File copyTestPluginTo(String testPluginName, File toDir) throws IOException { - File jar = TestProjectUtils.jarOf(testPluginName); - // file is copied because it's supposed to be moved by the test - FileUtils.copyFileToDirectory(jar, toDir); - return new File(toDir, jar.getName()); + private ServerPlugin newPlugin(String key) { + return new ServerPlugin(newPluginInfo(key), EXTERNAL, mock(Plugin.class), mock(FileAndMd5.class), mock(FileAndMd5.class), mock(ClassLoader.class)); } } diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java index 219653eb7c1..648bc614a5e 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java @@ -32,7 +32,6 @@ import org.sonar.updatecenter.common.UpdateCenter; import org.sonar.updatecenter.common.Version; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.guava.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -72,7 +71,7 @@ public class UpdateCenterClientTest { @Test public void ignore_connection_errors() { when(reader.readString(any(URI.class), eq(StandardCharsets.UTF_8))).thenThrow(new SonarException()); - assertThat(underTest.getUpdateCenter()).isAbsent(); + assertThat(underTest.getUpdateCenter()).isEmpty(); } @Test @@ -99,6 +98,6 @@ public class UpdateCenterClientTest { public void update_center_is_null_when_property_is_false() { settings.setProperty(ProcessProperties.Property.SONAR_UPDATECENTER_ACTIVATE.getKey(), false); - assertThat(underTest.getUpdateCenter()).isAbsent(); + assertThat(underTest.getUpdateCenter()).isEmpty(); } } diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java index fcffc869ba9..5609664ba29 100644 --- a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java +++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java @@ -19,12 +19,12 @@ */ package org.sonar.server.plugins; -import com.google.common.base.Optional; +import java.util.Optional; import org.junit.Test; import org.sonar.api.SonarRuntime; import org.sonar.updatecenter.common.UpdateCenter; -import static org.assertj.guava.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -36,12 +36,12 @@ public class UpdateCenterMatrixFactoryTest { @Test public void return_absent_update_center() { UpdateCenterClient updateCenterClient = mock(UpdateCenterClient.class); - when(updateCenterClient.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent()); + when(updateCenterClient.getUpdateCenter(anyBoolean())).thenReturn(Optional.empty()); underTest = new UpdateCenterMatrixFactory(updateCenterClient, mock(SonarRuntime.class), mock(InstalledPluginReferentialFactory.class)); Optional updateCenter = underTest.getUpdateCenter(false); - assertThat(updateCenter).isAbsent(); + assertThat(updateCenter).isEmpty(); } } diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java index 78609fa838d..045ce07e2de 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java @@ -33,8 +33,8 @@ import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.api.utils.log.Profiler; import org.sonar.server.platform.ServerFileSystem; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; /** * The file deploy/plugins/index.txt is required for old versions of SonarLint. @@ -48,11 +48,11 @@ public final class GeneratePluginIndex implements Startable { private static final Logger LOG = Loggers.get(GeneratePluginIndex.class); private final ServerFileSystem serverFs; - private final PluginFileSystem pluginFs; + private final ServerPluginRepository serverPluginRepository; - public GeneratePluginIndex(ServerFileSystem serverFs, PluginFileSystem pluginFs) { + public GeneratePluginIndex(ServerFileSystem serverFs, ServerPluginRepository serverPluginRepository) { this.serverFs = serverFs; - this.pluginFs = pluginFs; + this.serverPluginRepository = serverPluginRepository; } @Override @@ -71,7 +71,7 @@ public final class GeneratePluginIndex implements Startable { try { FileUtils.forceMkdir(indexFile.getParentFile()); try (Writer writer = new OutputStreamWriter(new FileOutputStream(indexFile), StandardCharsets.UTF_8)) { - for (InstalledPlugin plugin : pluginFs.getInstalledFiles()) { + for (ServerPlugin plugin : serverPluginRepository.getPlugins()) { writer.append(toRow(plugin)); writer.append(CharUtils.LF); } @@ -82,16 +82,15 @@ public final class GeneratePluginIndex implements Startable { } } - private static String toRow(InstalledPlugin file) { - StringBuilder sb = new StringBuilder(); - sb.append(file.getPluginInfo().getKey()) + private static String toRow(ServerPlugin file) { + return new StringBuilder().append(file.getPluginInfo().getKey()) .append(",") .append(file.getPluginInfo().isSonarLintSupported()) .append(",") - .append(file.getLoadedJar().getFile().getName()) + .append(file.getJar().getFile().getName()) .append("|") - .append(file.getLoadedJar().getMd5()); - return sb.toString(); + .append(file.getJar().getMd5()) + .toString(); } } diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java index 85710a00b12..9055589c4ee 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java @@ -32,8 +32,9 @@ import org.sonar.core.util.UuidFactory; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.plugin.PluginDto; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.server.plugins.PluginType; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; import static java.util.function.Function.identity; @@ -45,13 +46,13 @@ public class RegisterPlugins implements Startable { private static final Logger LOG = Loggers.get(RegisterPlugins.class); - private final PluginFileSystem pluginFileSystem; + private final ServerPluginRepository serverPluginRepository; private final DbClient dbClient; private final UuidFactory uuidFactory; private final System2 system; - public RegisterPlugins(PluginFileSystem pluginFileSystem, DbClient dbClient, UuidFactory uuidFactory, System2 system) { - this.pluginFileSystem = pluginFileSystem; + public RegisterPlugins(ServerPluginRepository serverPluginRepository, DbClient dbClient, UuidFactory uuidFactory, System2 system) { + this.serverPluginRepository = serverPluginRepository; this.dbClient = dbClient; this.uuidFactory = uuidFactory; this.system = system; @@ -74,7 +75,7 @@ public class RegisterPlugins implements Startable { try (DbSession dbSession = dbClient.openSession(false)) { Map allPreviousPluginsByKey = dbClient.pluginDao().selectAll(dbSession).stream() .collect(Collectors.toMap(PluginDto::getKee, identity())); - for (InstalledPlugin installed : pluginFileSystem.getInstalledFiles()) { + for (ServerPlugin installed : serverPluginRepository.getPlugins()) { PluginInfo info = installed.getPluginInfo(); PluginDto previousDto = allPreviousPluginsByKey.get(info.getKey()); if (previousDto == null) { @@ -83,15 +84,17 @@ public class RegisterPlugins implements Startable { .setUuid(uuidFactory.create()) .setKee(info.getKey()) .setBasePluginKey(info.getBasePlugin()) - .setFileHash(installed.getLoadedJar().getMd5()) + .setFileHash(installed.getJar().getMd5()) + .setType(toTypeDto(installed.getType())) .setCreatedAt(now) .setUpdatedAt(now); dbClient.pluginDao().insert(dbSession, pluginDto); - } else if (!previousDto.getFileHash().equals(installed.getLoadedJar().getMd5())) { + } else if (!previousDto.getFileHash().equals(installed.getJar().getMd5()) || !previousDto.getType().equals(toTypeDto(installed.getType()))) { LOG.debug("Update plugin {}", info.getKey()); previousDto .setBasePluginKey(info.getBasePlugin()) - .setFileHash(installed.getLoadedJar().getMd5()) + .setFileHash(installed.getJar().getMd5()) + .setType(toTypeDto(installed.getType())) .setUpdatedAt(now); dbClient.pluginDao().update(dbSession, previousDto); } @@ -101,4 +104,15 @@ public class RegisterPlugins implements Startable { } } + private static PluginDto.Type toTypeDto(PluginType type) { + switch (type) { + case EXTERNAL: + return PluginDto.Type.EXTERNAL; + case BUNDLED: + return PluginDto.Type.BUNDLED; + default: + throw new IllegalStateException("Unknown type: " + type); + } + } + } diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java index 73d262e9ff4..d93d0df6b5d 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java @@ -29,14 +29,15 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.sonar.core.platform.PluginInfo; import org.sonar.server.platform.ServerFileSystem; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.InstalledPlugin.FileAndMd5; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.sonar.server.plugins.PluginType.BUNDLED; public class GeneratePluginIndexTest { @@ -44,7 +45,7 @@ public class GeneratePluginIndexTest { public TemporaryFolder temp = new TemporaryFolder(); private ServerFileSystem serverFileSystem = mock(ServerFileSystem.class); - private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class); + private ServerPluginRepository serverPluginRepository = mock(ServerPluginRepository.class); private File index; @Before @@ -55,17 +56,17 @@ public class GeneratePluginIndexTest { @Test public void shouldWriteIndex() throws IOException { - InstalledPlugin javaPlugin = newInstalledPlugin("java", true); - InstalledPlugin gitPlugin = newInstalledPlugin("scmgit", false); - when(pluginFileSystem.getInstalledFiles()).thenReturn(asList(javaPlugin, gitPlugin)); + ServerPlugin javaPlugin = newInstalledPlugin("java", true); + ServerPlugin gitPlugin = newInstalledPlugin("scmgit", false); + when(serverPluginRepository.getPlugins()).thenReturn(asList(javaPlugin, gitPlugin)); - GeneratePluginIndex underTest = new GeneratePluginIndex(serverFileSystem, pluginFileSystem); + GeneratePluginIndex underTest = new GeneratePluginIndex(serverFileSystem, serverPluginRepository); underTest.start(); List lines = FileUtils.readLines(index); assertThat(lines).containsExactly( - "java,true," + javaPlugin.getLoadedJar().getFile().getName() + "|" + javaPlugin.getLoadedJar().getMd5(), - "scmgit,false," + gitPlugin.getLoadedJar().getFile().getName() + "|" + gitPlugin.getLoadedJar().getMd5()); + "java,true," + javaPlugin.getJar().getFile().getName() + "|" + javaPlugin.getJar().getMd5(), + "scmgit,false," + gitPlugin.getJar().getFile().getName() + "|" + gitPlugin.getJar().getMd5()); underTest.stop(); } @@ -77,12 +78,12 @@ public class GeneratePluginIndexTest { File wrongIndex = new File(wrongParent, "index.txt"); when(serverFileSystem.getPluginIndex()).thenReturn(wrongIndex); - new GeneratePluginIndex(serverFileSystem, pluginFileSystem).start(); + new GeneratePluginIndex(serverFileSystem, serverPluginRepository).start(); } - private InstalledPlugin newInstalledPlugin(String key, boolean supportSonarLint) throws IOException { + private ServerPlugin newInstalledPlugin(String key, boolean supportSonarLint) throws IOException { FileAndMd5 jar = new FileAndMd5(temp.newFile()); PluginInfo pluginInfo = new PluginInfo(key).setJarFile(jar.getFile()).setSonarLintSupported(supportSonarLint); - return new InstalledPlugin(pluginInfo, jar, null); + return new ServerPlugin(pluginInfo, BUNDLED, null, jar, null, null); } } diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java index 18d4b454f79..cb13a5200bb 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java @@ -35,8 +35,11 @@ import org.sonar.core.util.UuidFactory; import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.plugin.PluginDto; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.db.plugin.PluginDto.Type; +import org.sonar.server.plugins.PluginFilesAndMd5; +import org.sonar.server.plugins.PluginType; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; @@ -52,10 +55,10 @@ public class RegisterPluginsTest { public DbTester dbTester = DbTester.create(System2.INSTANCE); private final long now = 12345L; - private DbClient dbClient = dbTester.getDbClient(); - private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class); - private UuidFactory uuidFactory = mock(UuidFactory.class); - private System2 system2 = mock(System2.class); + private final DbClient dbClient = dbTester.getDbClient(); + private final ServerPluginRepository serverPluginRepository = mock(ServerPluginRepository.class); + private final UuidFactory uuidFactory = mock(UuidFactory.class); + private final System2 system2 = mock(System2.class); @Before public void setUp() { @@ -71,17 +74,17 @@ public class RegisterPluginsTest { FileUtils.write(fakeJavaJar, "fakejava", StandardCharsets.UTF_8); File fakeJavaCustomJar = temp.newFile(); FileUtils.write(fakeJavaCustomJar, "fakejavacustom", StandardCharsets.UTF_8); - when(pluginFileSystem.getInstalledFiles()).thenReturn(asList( + when(serverPluginRepository.getPlugins()).thenReturn(asList( newPlugin("java", fakeJavaJar, null), newPlugin("javacustom", fakeJavaCustomJar, "java"))); when(uuidFactory.create()).thenReturn("a").thenReturn("b").thenThrow(new IllegalStateException("Should be called only twice")); - RegisterPlugins register = new RegisterPlugins(pluginFileSystem, dbClient, uuidFactory, system2); + RegisterPlugins register = new RegisterPlugins(serverPluginRepository, dbClient, uuidFactory, system2); register.start(); Map pluginsByKey = selectAllPlugins(); assertThat(pluginsByKey).hasSize(2); - verify(pluginsByKey.get("java"), null, "bd451e47a1aa76e73da0359cef63dd63", now, now); - verify(pluginsByKey.get("javacustom"), "java", "de9b2de3ddc0680904939686c0dba5be", now, now); + verify(pluginsByKey.get("java"), Type.BUNDLED, null, "bd451e47a1aa76e73da0359cef63dd63", now, now); + verify(pluginsByKey.get("javacustom"), Type.BUNDLED, "java", "de9b2de3ddc0680904939686c0dba5be", now, now); register.stop(); } @@ -96,6 +99,7 @@ public class RegisterPluginsTest { .setKee("java") .setBasePluginKey(null) .setFileHash("bd451e47a1aa76e73da0359cef63dd63") + .setType(Type.BUNDLED) .setCreatedAt(1L) .setUpdatedAt(1L)); dbClient.pluginDao().insert(dbTester.getSession(), new PluginDto() @@ -103,39 +107,69 @@ public class RegisterPluginsTest { .setKee("javacustom") .setBasePluginKey("java") .setFileHash("de9b2de3ddc0680904939686c0dba5be") + .setType(Type.BUNDLED) .setCreatedAt(1L) .setUpdatedAt(1L)); + dbClient.pluginDao().insert(dbTester.getSession(), new PluginDto() + .setUuid("c") + .setKee("csharp") + .setBasePluginKey(null) + .setFileHash("a4813b6d879c4ec852747c175cdd6141") + .setType(Type.EXTERNAL) + .setCreatedAt(1L) + .setUpdatedAt(1L)); + dbClient.pluginDao().insert(dbTester.getSession(), new PluginDto() + .setUuid("d") + .setKee("new-measures") + .setBasePluginKey(null) + .setFileHash("6d24712cf701c41ce5eaa948e0bd6d22") + .setType(Type.EXTERNAL) + .setCreatedAt(1L) + .setUpdatedAt(1L)); + dbTester.commit(); File fakeJavaCustomJar = temp.newFile(); FileUtils.write(fakeJavaCustomJar, "fakejavacustomchanged", StandardCharsets.UTF_8); - when(pluginFileSystem.getInstalledFiles()).thenReturn(asList( - newPlugin("javacustom", fakeJavaCustomJar, "java2"))); - new RegisterPlugins(pluginFileSystem, dbClient, uuidFactory, system2).start(); + File fakeCSharpJar = temp.newFile(); + FileUtils.write(fakeCSharpJar, "fakecsharp", StandardCharsets.UTF_8); + + when(serverPluginRepository.getPlugins()).thenReturn(asList( + newPlugin("javacustom", PluginType.BUNDLED, fakeJavaCustomJar, "java2"), + // csharp plugin type changed + newPlugin("csharp", PluginType.BUNDLED, fakeCSharpJar, null))); + + new RegisterPlugins(serverPluginRepository, dbClient, uuidFactory, system2).start(); Map pluginsByKey = selectAllPlugins(); - assertThat(pluginsByKey).hasSize(2); - verify(pluginsByKey.get("java"), null, "bd451e47a1aa76e73da0359cef63dd63", 1L, 1L); - verify(pluginsByKey.get("javacustom"), "java2", "d22091cff5155e892cfe2f9dab51f811", 1L, now); + assertThat(pluginsByKey).hasSize(4); + verify(pluginsByKey.get("java"), Type.BUNDLED, null, "bd451e47a1aa76e73da0359cef63dd63", 1L, 1L); + verify(pluginsByKey.get("javacustom"), Type.BUNDLED, "java2", "d22091cff5155e892cfe2f9dab51f811", 1L, now); + verify(pluginsByKey.get("csharp"), Type.BUNDLED, null, "a4813b6d879c4ec852747c175cdd6141", 1L, now); + verify(pluginsByKey.get("new-measures"), Type.EXTERNAL, null, "6d24712cf701c41ce5eaa948e0bd6d22", 1L, 1L); + } + + private static ServerPlugin newPlugin(String key, File file, @Nullable String basePlugin) { + return newPlugin(key, PluginType.BUNDLED, file, basePlugin); } - private static InstalledPlugin newPlugin(String key, File file, @Nullable String basePlugin) { - InstalledPlugin.FileAndMd5 jar = new InstalledPlugin.FileAndMd5(file); + private static ServerPlugin newPlugin(String key, PluginType type, File file, @Nullable String basePlugin) { + PluginFilesAndMd5.FileAndMd5 jar = new PluginFilesAndMd5.FileAndMd5(file); PluginInfo info = new PluginInfo(key) .setBasePlugin(basePlugin) .setJarFile(file); - return new InstalledPlugin(info, jar, null); + return new ServerPlugin(info, PluginType.BUNDLED, null, jar, null, null); } private Map selectAllPlugins() { - return dbTester.getDbClient().pluginDao().selectAll(dbTester.getSession()) - .stream() + return dbTester.getDbClient().pluginDao().selectAll(dbTester.getSession()).stream() .collect(uniqueIndex(PluginDto::getKee)); } - private void verify(PluginDto java, @Nullable String basePluginKey, String fileHash, @Nullable Long createdAt, long updatedAt) { + private void verify(PluginDto java, Type type, @Nullable String basePluginKey, String fileHash, @Nullable Long createdAt, long updatedAt) { assertThat(java.getBasePluginKey()).isEqualTo(basePluginKey); + assertThat(java.getType()).isEqualTo(type); assertThat(java.getFileHash()).isEqualTo(fileHash); assertThat(java.getCreatedAt()).isEqualTo(createdAt); assertThat(java.getUpdatedAt()).isEqualTo(updatedAt); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/UpgradesAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/UpgradesAction.java index 042fe1bbbfe..7dee70266d1 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/UpgradesAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/UpgradesAction.java @@ -19,9 +19,9 @@ */ package org.sonar.server.platform.ws; -import com.google.common.base.Optional; import com.google.common.io.Resources; import java.util.List; +import java.util.Optional; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/AvailableAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/AvailableAction.java index 108210c2f2f..dcfc53c9a56 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/AvailableAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/AvailableAction.java @@ -19,10 +19,10 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableSortedSet; import com.google.common.io.Resources; import java.util.Collection; +import java.util.Optional; +import java.util.stream.Collectors; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -94,10 +94,7 @@ public class AvailableAction implements PluginsWsAction { } private static Collection retrieveAvailablePlugins(UpdateCenter updateCenter) { - return ImmutableSortedSet.copyOf( - NAME_KEY_PLUGIN_UPDATE_ORDERING, - updateCenter.findAvailablePlugins() - ); + return updateCenter.findAvailablePlugins().stream().sorted(NAME_KEY_PLUGIN_UPDATE_ORDERING).collect(Collectors.toList()); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/DownloadAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/DownloadAction.java index c837f38ac07..fdccdd69131 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/DownloadAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/DownloadAction.java @@ -27,9 +27,9 @@ import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.server.exceptions.NotFoundException; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.InstalledPlugin.FileAndMd5; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; public class DownloadAction implements PluginsWsAction { @@ -37,10 +37,10 @@ public class DownloadAction implements PluginsWsAction { private static final String ACCEPT_COMPRESSIONS_PARAM = "acceptCompressions"; private static final String PLUGIN_PARAM = "plugin"; - private final PluginFileSystem pluginFileSystem; + private final ServerPluginRepository pluginRepository; - public DownloadAction(PluginFileSystem pluginFileSystem) { - this.pluginFileSystem = pluginFileSystem; + public DownloadAction(ServerPluginRepository pluginRepository) { + this.pluginRepository = pluginRepository; } @Override @@ -64,22 +64,22 @@ public class DownloadAction implements PluginsWsAction { public void handle(Request request, Response response) throws Exception { String pluginKey = request.mandatoryParam(PLUGIN_PARAM); - Optional file = pluginFileSystem.getInstalledPlugin(pluginKey); + Optional file = pluginRepository.findPlugin(pluginKey); if (!file.isPresent()) { throw new NotFoundException("Plugin " + pluginKey + " not found"); } FileAndMd5 downloadedFile; - FileAndMd5 compressedJar = file.get().getCompressedJar(); + FileAndMd5 compressedJar = file.get().getCompressed(); if (compressedJar != null && PACK200.equals(request.param(ACCEPT_COMPRESSIONS_PARAM))) { response.stream().setMediaType("application/octet-stream"); response.setHeader("Sonar-Compression", PACK200); - response.setHeader("Sonar-UncompressedMD5", file.get().getLoadedJar().getMd5()); + response.setHeader("Sonar-UncompressedMD5", file.get().getJar().getMd5()); downloadedFile = compressedJar; } else { response.stream().setMediaType("application/java-archive"); - downloadedFile = file.get().getLoadedJar(); + downloadedFile = file.get().getJar(); } response.setHeader("Sonar-MD5", downloadedFile.getMd5()); try (InputStream input = FileUtils.openInputStream(downloadedFile.getFile())) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstallAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstallAction.java index 7ddfef1d761..ee9963cb9f5 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstallAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstallAction.java @@ -19,8 +19,8 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; import java.util.Objects; +import java.util.Optional; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstalledAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstalledAction.java index 485794fc62a..519b5b6872f 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstalledAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstalledAction.java @@ -20,12 +20,13 @@ package org.sonar.server.plugins.ws; import com.google.common.io.Resources; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.SortedSet; import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; @@ -34,8 +35,10 @@ import org.sonar.api.utils.text.JsonWriter; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.plugin.PluginDto; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.db.plugin.PluginDto.Type; +import org.sonar.server.plugins.PluginType; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; import org.sonar.server.plugins.UpdateCenterMatrixFactory; import org.sonar.updatecenter.common.Plugin; @@ -54,14 +57,14 @@ import static org.sonar.server.plugins.ws.PluginWSCommons.compatiblePluginsByKey public class InstalledAction implements PluginsWsAction { private static final String ARRAY_PLUGINS = "plugins"; private static final String FIELD_CATEGORY = "category"; + private static final String PARAM_TYPE = "type"; - private final PluginFileSystem pluginFileSystem; + private final ServerPluginRepository serverPluginRepository; private final UpdateCenterMatrixFactory updateCenterMatrixFactory; private final DbClient dbClient; - public InstalledAction(PluginFileSystem pluginFileSystem, - UpdateCenterMatrixFactory updateCenterMatrixFactory, DbClient dbClient) { - this.pluginFileSystem = pluginFileSystem; + public InstalledAction(ServerPluginRepository serverPluginRepository, UpdateCenterMatrixFactory updateCenterMatrixFactory, DbClient dbClient) { + this.serverPluginRepository = serverPluginRepository; this.updateCenterMatrixFactory = updateCenterMatrixFactory; this.dbClient = dbClient; } @@ -72,12 +75,12 @@ public class InstalledAction implements PluginsWsAction { .setDescription("Get the list of all the plugins installed on the SonarQube instance, sorted by plugin name.") .setSince("5.2") .setChangelog( + new Change("8.0", "The 'documentationPath' field is added"), + new Change("7.0", "The fields 'compressedHash' and 'compressedFilename' are added"), new Change("6.6", "The 'filename' field is added"), new Change("6.6", "The 'fileHash' field is added"), new Change("6.6", "The 'sonarLintSupported' field is added"), - new Change("6.6", "The 'updatedAt' field is added"), - new Change("7.0", "The fields 'compressedHash' and 'compressedFilename' are added"), - new Change("8.0", "The 'documentationPath' field is added")) + new Change("6.6", "The 'updatedAt' field is added")) .setHandler(this) .setResponseExample(Resources.getResource(this.getClass(), "example-installed_plugins.json")); @@ -87,11 +90,18 @@ public class InstalledAction implements PluginsWsAction { "
  • %s - category as defined in the Update Center. A connection to the Update Center is needed
  • " + "", FIELD_CATEGORY)) .setSince("5.6"); + + action.createParam(PARAM_TYPE) + .setInternal(true) + .setSince("8.5") + .setPossibleValues(Type.values()) + .setDescription("Allows to filter plugins by type"); } @Override public void handle(Request request, Response response) throws Exception { - Collection installedPlugins = loadInstalledPlugins(); + String typeParam = request.param(PARAM_TYPE); + SortedSet installedPlugins = loadInstalledPlugins(typeParam); Map dtosByKey; try (DbSession dbSession = dbClient.openSession(false)) { dtosByKey = dbClient.pluginDao().selectAll(dbSession).stream().collect(toMap(PluginDto::getKee, Function.identity())); @@ -106,7 +116,7 @@ public class InstalledAction implements PluginsWsAction { json.name(ARRAY_PLUGINS); json.beginArray(); - for (InstalledPlugin installedPlugin : copyOf(NAME_KEY_COMPARATOR, installedPlugins)) { + for (ServerPlugin installedPlugin : installedPlugins) { PluginDto pluginDto = dtosByKey.get(installedPlugin.getPluginInfo().getKey()); Objects.requireNonNull(pluginDto, () -> format("Plugin %s is installed but not in DB", installedPlugin.getPluginInfo().getKey())); Plugin updateCenterPlugin = updateCenterPlugins.get(installedPlugin.getPluginInfo().getKey()); @@ -117,7 +127,12 @@ public class InstalledAction implements PluginsWsAction { json.close(); } - private SortedSet loadInstalledPlugins() { - return copyOf(NAME_KEY_COMPARATOR, pluginFileSystem.getInstalledFiles()); + private SortedSet loadInstalledPlugins(@Nullable String typeParam) { + if (typeParam != null) { + return copyOf(NAME_KEY_COMPARATOR, serverPluginRepository.getPlugins().stream() + .filter(serverPlugin -> serverPlugin.getType().equals(PluginType.valueOf(typeParam))) + .collect(Collectors.toSet())); + } + return copyOf(NAME_KEY_COMPARATOR, serverPluginRepository.getPlugins()); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PendingAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PendingAction.java index 5f1726e56f4..220f142ea8d 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PendingAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PendingAction.java @@ -19,15 +19,13 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Function; -import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import java.util.Set; -import javax.annotation.Nonnull; +import java.util.stream.Collectors; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; @@ -41,8 +39,6 @@ import org.sonar.server.plugins.UpdateCenterMatrixFactory; import org.sonar.server.user.UserSession; import org.sonar.updatecenter.common.Plugin; -import static com.google.common.collect.FluentIterable.from; -import static com.google.common.collect.ImmutableSet.copyOf; import static com.google.common.io.Resources.getResource; import static org.sonar.server.plugins.ws.PluginWSCommons.NAME_KEY_PLUGIN_METADATA_COMPARATOR; import static org.sonar.server.plugins.ws.PluginWSCommons.categoryOrNull; @@ -59,15 +55,15 @@ public class PendingAction implements PluginsWsAction { private final UserSession userSession; private final PluginDownloader pluginDownloader; - private final ServerPluginRepository installer; + private final ServerPluginRepository serverPluginRepository; private final UpdateCenterMatrixFactory updateCenterMatrixFactory; private final PluginUninstaller pluginUninstaller; public PendingAction(UserSession userSession, PluginDownloader pluginDownloader, - ServerPluginRepository installer, PluginUninstaller pluginUninstaller, UpdateCenterMatrixFactory updateCenterMatrixFactory) { + ServerPluginRepository serverPluginRepository, PluginUninstaller pluginUninstaller, UpdateCenterMatrixFactory updateCenterMatrixFactory) { this.userSession = userSession; this.pluginDownloader = pluginDownloader; - this.installer = installer; + this.serverPluginRepository = serverPluginRepository; this.pluginUninstaller = pluginUninstaller; this.updateCenterMatrixFactory = updateCenterMatrixFactory; } @@ -100,13 +96,13 @@ public class PendingAction implements PluginsWsAction { private void writePlugins(JsonWriter json, Map compatiblePluginsByKey) { Collection uninstalledPlugins = pluginUninstaller.getUninstalledPlugins(); Collection downloadedPlugins = pluginDownloader.getDownloadedPlugins(); - Collection installedPlugins = installer.getPluginInfos(); - MatchPluginKeys matchPluginKeys = new MatchPluginKeys(from(installedPlugins).transform(PluginInfoToKey.INSTANCE).toSet()); + Collection installedPlugins = serverPluginRepository.getPluginInfos(); + Set installedPluginKeys = installedPlugins.stream().map(PluginInfo::getKey).collect(Collectors.toSet()); Collection newPlugins = new ArrayList<>(); Collection updatedPlugins = new ArrayList<>(); for (PluginInfo pluginInfo : downloadedPlugins) { - if (matchPluginKeys.apply(pluginInfo)) { + if (installedPluginKeys.contains(pluginInfo.getKey())) { updatedPlugins.add(pluginInfo); } else { newPlugins.add(pluginInfo); @@ -127,26 +123,4 @@ public class PendingAction implements PluginsWsAction { } json.endArray(); } - - private enum PluginInfoToKey implements Function { - INSTANCE; - - @Override - public String apply(@Nonnull PluginInfo input) { - return input.getKey(); - } - } - - private static class MatchPluginKeys implements Predicate { - private final Set pluginKeys; - - private MatchPluginKeys(Collection pluginKeys) { - this.pluginKeys = copyOf(pluginKeys); - } - - @Override - public boolean apply(@Nonnull PluginInfo input) { - return pluginKeys.contains(input.getKey()); - } - } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PluginWSCommons.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PluginWSCommons.java index 9704a8c89b0..9caea752d46 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PluginWSCommons.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PluginWSCommons.java @@ -20,19 +20,19 @@ package org.sonar.server.plugins.ws; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.utils.text.JsonWriter; import org.sonar.core.platform.PluginInfo; import org.sonar.db.plugin.PluginDto; -import org.sonar.server.plugins.InstalledPlugin; +import org.sonar.server.plugins.ServerPlugin; import org.sonar.server.plugins.UpdateCenterMatrixFactory; import org.sonar.server.plugins.edition.EditionBundledPlugins; import org.sonar.updatecenter.common.Artifact; @@ -78,8 +78,8 @@ public class PluginWSCommons { public static final Ordering NAME_KEY_PLUGIN_METADATA_COMPARATOR = Ordering.natural() .onResultOf(PluginInfo::getName) .compound(Ordering.natural().onResultOf(PluginInfo::getKey)); - public static final Comparator NAME_KEY_COMPARATOR = Comparator - .comparing((java.util.function.Function) installedPluginFile -> installedPluginFile.getPluginInfo().getName()) + public static final Comparator NAME_KEY_COMPARATOR = Comparator + .comparing((java.util.function.Function) installedPluginFile -> installedPluginFile.getPluginInfo().getName()) .thenComparing(f -> f.getPluginInfo().getKey()); public static final Comparator NAME_KEY_PLUGIN_ORDERING = Ordering.from(CASE_INSENSITIVE_ORDER) .onResultOf(Plugin::getName) @@ -92,7 +92,7 @@ public class PluginWSCommons { // prevent instantiation } - public static void writePluginInfo(JsonWriter json, PluginInfo pluginInfo, @Nullable String category, @Nullable PluginDto pluginDto, @Nullable InstalledPlugin installedFile) { + public static void writePluginInfo(JsonWriter json, PluginInfo pluginInfo, @Nullable String category, @Nullable PluginDto pluginDto, @Nullable ServerPlugin installedFile) { json.beginObject(); json.prop(PROPERTY_KEY, pluginInfo.getKey()); json.prop(PROPERTY_NAME, pluginInfo.getName()); @@ -115,9 +115,9 @@ public class PluginWSCommons { json.prop(PROPERTY_UPDATED_AT, pluginDto.getUpdatedAt()); } if (installedFile != null) { - json.prop(PROPERTY_FILENAME, installedFile.getLoadedJar().getFile().getName()); + json.prop(PROPERTY_FILENAME, installedFile.getJar().getFile().getName()); json.prop(PROPERTY_SONARLINT_SUPPORTED, installedFile.getPluginInfo().isSonarLintSupported()); - json.prop(PROPERTY_HASH, installedFile.getLoadedJar().getMd5()); + json.prop(PROPERTY_HASH, installedFile.getJar().getMd5()); } json.endObject(); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UninstallAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UninstallAction.java index 6fc841762ee..51933509285 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UninstallAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UninstallAction.java @@ -22,26 +22,19 @@ package org.sonar.server.plugins.ws; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; -import org.sonar.core.platform.PluginInfo; import org.sonar.server.plugins.PluginUninstaller; -import org.sonar.server.plugins.ServerPluginRepository; import org.sonar.server.user.UserSession; -import static java.lang.String.format; -import static org.sonar.server.plugins.edition.EditionBundledPlugins.isEditionBundled; - /** * Implementation of the {@code uninstall} action for the Plugins WebService. */ public class UninstallAction implements PluginsWsAction { private static final String PARAM_KEY = "key"; - private final ServerPluginRepository serverPluginRepository; private final PluginUninstaller pluginUninstaller; private final UserSession userSession; - public UninstallAction(ServerPluginRepository serverPluginRepository, PluginUninstaller pluginUninstaller, UserSession userSession) { - this.serverPluginRepository = serverPluginRepository; + public UninstallAction(PluginUninstaller pluginUninstaller, UserSession userSession) { this.pluginUninstaller = pluginUninstaller; this.userSession = userSession; } @@ -64,18 +57,7 @@ public class UninstallAction implements PluginsWsAction { @Override public void handle(Request request, Response response) throws Exception { userSession.checkIsSystemAdministrator(); - - String key = request.mandatoryParam(PARAM_KEY); - PluginInfo pluginInfo = serverPluginRepository.getPluginInfo(key); - if (pluginInfo != null) { - if (isEditionBundled(pluginInfo)) { - throw new IllegalArgumentException(format( - "SonarSource commercial plugin with key '%s' can only be uninstalled as part of a SonarSource edition", - pluginInfo.getKey())); - } - pluginUninstaller.uninstall(key); - } + pluginUninstaller.uninstall(request.mandatoryParam(PARAM_KEY)); response.noContent(); } - } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdateAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdateAction.java index 69c802e9a56..d142e15854b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdateAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdateAction.java @@ -19,11 +19,8 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; +import java.util.Optional; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -86,11 +83,9 @@ public class UpdateAction implements PluginsWsAction { PluginUpdate pluginUpdate = MISSING_PLUGIN; if (updateCenter.isPresent()) { - pluginUpdate = Iterables.find( - updateCenter.get().findPluginUpdates(), - new PluginKeyPredicate(key), - MISSING_PLUGIN - ); + pluginUpdate = updateCenter.get().findPluginUpdates().stream() + .filter(update -> update != null && key.equals(update.getPlugin().getKey())) + .findFirst().orElse(MISSING_PLUGIN); } if (pluginUpdate == MISSING_PLUGIN) { @@ -99,17 +94,4 @@ public class UpdateAction implements PluginsWsAction { } return pluginUpdate; } - - private static class PluginKeyPredicate implements Predicate { - private final String key; - - public PluginKeyPredicate(String key) { - this.key = key; - } - - @Override - public boolean apply(@Nullable PluginUpdate input) { - return input != null && key.equals(input.getPlugin().getKey()); - } - } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdatesAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdatesAction.java index b6201298727..aef313273c1 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdatesAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdatesAction.java @@ -19,12 +19,12 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import com.google.common.io.Resources; import java.util.Collection; import java.util.List; +import java.util.Optional; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/language/LanguageValidationTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/language/LanguageValidationTest.java index 3f1af6853cb..4dd4d6ce7b9 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/language/LanguageValidationTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/language/LanguageValidationTest.java @@ -25,6 +25,7 @@ import org.junit.rules.ExpectedException; import org.sonar.api.resources.Language; import org.sonar.server.plugins.ServerPluginRepository; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -43,11 +44,11 @@ public class LanguageValidationTest { when(repo.getPluginKey(lang1)).thenReturn("plugin1"); when(repo.getPluginKey(lang2)).thenReturn("plugin2"); - exception.expect(IllegalStateException.class); - exception.expectMessage("There are two languages declared with the same key 'key' declared by the plugins 'plugin1' and 'plugin2'. " - + "Please uninstall one of the conflicting plugins."); LanguageValidation languageValidation = new LanguageValidation(repo, lang1, lang2); - languageValidation.start(); + assertThatThrownBy(languageValidation::start) + .isInstanceOf(IllegalStateException.class) + .hasMessage("There are two languages declared with the same key 'key' declared by the plugins 'plugin1' and 'plugin2'. " + + "Please uninstall one of the conflicting plugins."); } @Test @@ -67,6 +68,7 @@ public class LanguageValidationTest { when(lang2.getKey()).thenReturn("key2"); ServerPluginRepository repo = mock(ServerPluginRepository.class); + when(repo.getPluginKey(lang1)).thenReturn("plugin1"); when(repo.getPluginKey(lang2)).thenReturn("plugin2"); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/UpgradesActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/UpgradesActionTest.java index 1ba9387ddf2..fecc923f3e4 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/UpgradesActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/UpgradesActionTest.java @@ -19,7 +19,7 @@ */ package org.sonar.server.platform.ws; -import com.google.common.base.Optional; +import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.sonar.api.server.ws.WebService; @@ -114,7 +114,7 @@ public class UpgradesActionTest { @Test public void empty_array_is_returned_when_update_center_is_unavailable() { - when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent()); + when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.empty()); TestResponse response = tester.newRequest().execute(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AbstractUpdateCenterBasedPluginsWsActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AbstractUpdateCenterBasedPluginsWsActionTest.java index a893e478e61..60d60e683e3 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AbstractUpdateCenterBasedPluginsWsActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AbstractUpdateCenterBasedPluginsWsActionTest.java @@ -19,7 +19,7 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; +import java.util.Optional; import org.junit.Before; import org.sonar.api.utils.DateUtils; import org.sonar.server.plugins.UpdateCenterMatrixFactory; diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AvailableActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AvailableActionTest.java index 726d7ce490b..5939880a23c 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AvailableActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AvailableActionTest.java @@ -19,7 +19,7 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; +import java.util.Optional; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -138,7 +138,7 @@ public class AvailableActionTest extends AbstractUpdateCenterBasedPluginsWsActio @Test public void empty_array_is_returned_when_update_center_is_not_accessible() { logInAsSystemAdministrator(); - when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent()); + when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.empty()); TestResponse response = tester.newRequest().execute(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/DownloadActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/DownloadActionTest.java index 3cc798a12be..1a579ae1192 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/DownloadActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/DownloadActionTest.java @@ -31,14 +31,17 @@ import org.junit.rules.TemporaryFolder; import org.sonar.api.server.ws.WebService; import org.sonar.core.platform.PluginInfo; import org.sonar.server.exceptions.NotFoundException; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.InstalledPlugin.FileAndMd5; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; +import org.sonar.server.plugins.PluginType; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; +import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsAction; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -49,9 +52,9 @@ public class DownloadActionTest { @Rule public ExpectedException expectedException = ExpectedException.none(); - private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class); - private WsAction underTest = new DownloadAction(pluginFileSystem); - private WsActionTester tester = new WsActionTester(underTest); + private final ServerPluginRepository serverPluginRepository = mock(ServerPluginRepository.class); + private final WsAction underTest = new DownloadAction(serverPluginRepository); + private final WsActionTester tester = new WsActionTester(underTest); @Test public void test_definition() { @@ -66,90 +69,87 @@ public class DownloadActionTest { @Test public void return_404_if_plugin_not_found() { - when(pluginFileSystem.getInstalledPlugin("foo")).thenReturn(Optional.empty()); + when(serverPluginRepository.findPlugin("foo")).thenReturn(Optional.empty()); - expectedException.expect(NotFoundException.class); - expectedException.expectMessage("Plugin foo not found"); - - tester.newRequest() - .setParam("plugin", "foo") - .execute(); + TestRequest request = tester.newRequest().setParam("plugin", "foo"); + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessage("Plugin foo not found"); } @Test public void return_jar_if_plugin_exists() throws Exception { - InstalledPlugin plugin = newPlugin(); - when(pluginFileSystem.getInstalledPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); - + ServerPlugin plugin = newPlugin(); + when(serverPluginRepository.findPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); TestResponse response = tester.newRequest() .setParam("plugin", plugin.getPluginInfo().getKey()) .execute(); - assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getLoadedJar().getMd5()); + assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getJar().getMd5()); assertThat(response.getHeader("Sonar-Compression")).isNull(); assertThat(response.getMediaType()).isEqualTo("application/java-archive"); - verifySameContent(response, plugin.getLoadedJar().getFile()); + verifySameContent(response, plugin.getJar().getFile()); } @Test public void return_uncompressed_jar_if_client_does_not_accept_compression() throws Exception { - InstalledPlugin plugin = newCompressedPlugin(); - when(pluginFileSystem.getInstalledPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); + ServerPlugin plugin = newCompressedPlugin(); + when(serverPluginRepository.findPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); TestResponse response = tester.newRequest() .setParam("plugin", plugin.getPluginInfo().getKey()) .execute(); - assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getLoadedJar().getMd5()); + assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getJar().getMd5()); assertThat(response.getHeader("Sonar-Compression")).isNull(); assertThat(response.getHeader("Sonar-UncompressedMD5")).isNull(); assertThat(response.getMediaType()).isEqualTo("application/java-archive"); - verifySameContent(response, plugin.getLoadedJar().getFile()); + verifySameContent(response, plugin.getJar().getFile()); } @Test public void return_uncompressed_jar_if_client_requests_unsupported_compression() throws Exception { - InstalledPlugin plugin = newCompressedPlugin(); - when(pluginFileSystem.getInstalledPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); + ServerPlugin plugin = newCompressedPlugin(); + when(serverPluginRepository.findPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); TestResponse response = tester.newRequest() .setParam("plugin", plugin.getPluginInfo().getKey()) .setParam("acceptCompressions", "zip") .execute(); - assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getLoadedJar().getMd5()); + assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getJar().getMd5()); assertThat(response.getHeader("Sonar-Compression")).isNull(); assertThat(response.getHeader("Sonar-UncompressedMD5")).isNull(); assertThat(response.getMediaType()).isEqualTo("application/java-archive"); - verifySameContent(response, plugin.getLoadedJar().getFile()); + verifySameContent(response, plugin.getJar().getFile()); } @Test public void return_compressed_jar_if_client_accepts_pack200() throws Exception { - InstalledPlugin plugin = newCompressedPlugin(); - when(pluginFileSystem.getInstalledPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); + ServerPlugin plugin = newCompressedPlugin(); + when(serverPluginRepository.findPlugin(plugin.getPluginInfo().getKey())).thenReturn(Optional.of(plugin)); TestResponse response = tester.newRequest() .setParam("plugin", plugin.getPluginInfo().getKey()) .setParam("acceptCompressions", "pack200") .execute(); - assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getCompressedJar().getMd5()); - assertThat(response.getHeader("Sonar-UncompressedMD5")).isEqualTo(plugin.getLoadedJar().getMd5()); + assertThat(response.getHeader("Sonar-MD5")).isEqualTo(plugin.getCompressed().getMd5()); + assertThat(response.getHeader("Sonar-UncompressedMD5")).isEqualTo(plugin.getJar().getMd5()); assertThat(response.getHeader("Sonar-Compression")).isEqualTo("pack200"); assertThat(response.getMediaType()).isEqualTo("application/octet-stream"); - verifySameContent(response, plugin.getCompressedJar().getFile()); + verifySameContent(response, plugin.getCompressed().getFile()); } - private InstalledPlugin newPlugin() throws IOException { + private ServerPlugin newPlugin() throws IOException { FileAndMd5 jar = new FileAndMd5(temp.newFile()); - return new InstalledPlugin(new PluginInfo("foo"), jar, null); + return new ServerPlugin(new PluginInfo("foo"), PluginType.BUNDLED, null, jar, null, null); } - private InstalledPlugin newCompressedPlugin() throws IOException { + private ServerPlugin newCompressedPlugin() throws IOException { FileAndMd5 jar = new FileAndMd5(temp.newFile()); FileAndMd5 compressedJar = new FileAndMd5(temp.newFile()); - return new InstalledPlugin(new PluginInfo("foo"), jar, compressedJar); + return new ServerPlugin(new PluginInfo("foo"), PluginType.BUNDLED, null, jar, compressedJar, null); } private static void verifySameContent(TestResponse response, File file) throws IOException { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstallActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstallActionTest.java index 172c52ffa74..8c73582d38a 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstallActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstallActionTest.java @@ -19,11 +19,11 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Optional; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -50,7 +50,6 @@ import static org.mockito.Mockito.when; @RunWith(DataProviderRunner.class) public class InstallActionTest { - private static final String DUMMY_CONTROLLER_KEY = "dummy"; private static final String ACTION_KEY = "install"; private static final String KEY_PARAM = "key"; private static final String PLUGIN_KEY = "pluginKey"; @@ -153,7 +152,7 @@ public class InstallActionTest { @Test public void IAE_is_raised_when_update_center_is_unavailable() { logInAsSystemAdministrator(); - when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent()); + when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.empty()); expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("No plugin with key 'pluginKey'"); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstalledActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstalledActionTest.java index dae4f95aebc..11e833ff1d9 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstalledActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstalledActionTest.java @@ -19,14 +19,17 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; import com.hazelcast.internal.json.Json; +import com.hazelcast.internal.json.JsonArray; import com.hazelcast.internal.json.JsonObject; +import com.hazelcast.internal.json.JsonValue; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; import java.util.Random; import org.junit.Rule; import org.junit.Test; @@ -38,9 +41,11 @@ import org.sonar.api.server.ws.WebService.Action; import org.sonar.api.utils.System2; import org.sonar.core.platform.PluginInfo; import org.sonar.db.DbTester; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.InstalledPlugin.FileAndMd5; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.db.plugin.PluginDto.Type; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; +import org.sonar.server.plugins.PluginType; +import org.sonar.server.plugins.ServerPlugin; +import org.sonar.server.plugins.ServerPluginRepository; import org.sonar.server.plugins.UpdateCenterMatrixFactory; import org.sonar.server.ws.WsActionTester; import org.sonar.updatecenter.common.Plugin; @@ -52,6 +57,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.sonar.test.JsonAssert.assertJson; @@ -70,8 +76,8 @@ public class InstalledActionTest { public DbTester db = DbTester.create(System2.INSTANCE); private UpdateCenterMatrixFactory updateCenterMatrixFactory = mock(UpdateCenterMatrixFactory.class, RETURNS_DEEP_STUBS); - private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class); - private InstalledAction underTest = new InstalledAction(pluginFileSystem, updateCenterMatrixFactory, db.getDbClient()); + private ServerPluginRepository serverPluginRepository = mock(ServerPluginRepository.class); + private InstalledAction underTest = new InstalledAction(serverPluginRepository, updateCenterMatrixFactory, db.getDbClient()); private WsActionTester tester = new WsActionTester(underTest); @DataProvider @@ -106,16 +112,70 @@ public class InstalledActionTest { @Test public void empty_array_when_update_center_is_unavailable() { - when(updateCenterMatrixFactory.getUpdateCenter(false)).thenReturn(Optional.absent()); + when(updateCenterMatrixFactory.getUpdateCenter(false)).thenReturn(Optional.empty()); String response = tester.newRequest().execute().getInput(); assertJson(response).withStrictArrayOrder().isSimilarTo(JSON_EMPTY_PLUGIN_LIST); } + @Test + public void filter_by_plugin_type() throws IOException { + when(serverPluginRepository.getPlugins()).thenReturn( + Arrays.asList( + newInstalledPlugin(new PluginInfo("foo-external-1") + .setName("foo-external-1"), + PluginType.EXTERNAL), + newInstalledPlugin(new PluginInfo("foo-bundled-1") + .setName("foo-bundled-1"), + PluginType.BUNDLED), + newInstalledPlugin(new PluginInfo("foo-external-2") + .setName("foo-external-2"), + PluginType.EXTERNAL))); + db.pluginDbTester().insertPlugin( + p -> p.setKee("foo-external-1"), + p -> p.setType(Type.EXTERNAL), + p -> p.setUpdatedAt(100L)); + db.pluginDbTester().insertPlugin( + p -> p.setKee("foo-bundled-1"), + p -> p.setType(Type.BUNDLED), + p -> p.setUpdatedAt(101L)); + db.pluginDbTester().insertPlugin( + p -> p.setKee("foo-external-2"), + p -> p.setType(Type.EXTERNAL), + p -> p.setUpdatedAt(102L)); + + // no type param + String response = tester.newRequest().execute().getInput(); + + JsonArray jsonArray = Json.parse(response).asObject().get("plugins").asArray(); + assertThat(jsonArray).hasSize(3); + assertThat(jsonArray).extracting(JsonValue::asObject) + .extracting(members -> members.get("key").asString()) + .containsExactlyInAnyOrder("foo-external-1", "foo-bundled-1", "foo-external-2"); + + // type param == BUNDLED + response = tester.newRequest().setParam("type", "BUNDLED").execute().getInput(); + + jsonArray = Json.parse(response).asObject().get("plugins").asArray(); + assertThat(jsonArray).hasSize(1); + assertThat(jsonArray).extracting(JsonValue::asObject) + .extracting(members -> members.get("key").asString()) + .containsExactlyInAnyOrder("foo-bundled-1"); + + // type param == EXTERNAL + response = tester.newRequest().setParam("type", "EXTERNAL").execute().getInput(); + + jsonArray = Json.parse(response).asObject().get("plugins").asArray(); + assertThat(jsonArray).hasSize(2); + assertThat(jsonArray).extracting(JsonValue::asObject) + .extracting(members -> members.get("key").asString()) + .containsExactlyInAnyOrder("foo-external-1", "foo-external-2"); + } + @Test public void empty_fields_are_not_serialized_to_json() throws IOException { - when(pluginFileSystem.getInstalledFiles()).thenReturn( + when(serverPluginRepository.getPlugins()).thenReturn( singletonList(newInstalledPlugin(new PluginInfo("foo") .setName("") .setDescription("") @@ -127,6 +187,7 @@ public class InstalledActionTest { .setIssueTrackerUrl("")))); db.pluginDbTester().insertPlugin( p -> p.setKee("foo"), + p -> p.setType(Type.EXTERNAL), p -> p.setUpdatedAt(100L)); String response = tester.newRequest().execute().getInput(); @@ -139,23 +200,26 @@ public class InstalledActionTest { assertThat(json.get("organizationUrl")).isNull(); assertThat(json.get("homepageUrl")).isNull(); assertThat(json.get("issueTrackerUrl")).isNull(); + } + private ServerPlugin newInstalledPlugin(PluginInfo plugin) throws IOException { + return newInstalledPlugin(plugin, PluginType.BUNDLED); } - private InstalledPlugin newInstalledPlugin(PluginInfo plugin) throws IOException { + private ServerPlugin newInstalledPlugin(PluginInfo plugin, PluginType type) throws IOException { FileAndMd5 jar = new FileAndMd5(temp.newFile()); - return new InstalledPlugin(plugin.setJarFile(jar.getFile()), jar, null); + return new ServerPlugin(plugin, type, null, jar, null, null); } - private InstalledPlugin newInstalledPluginWithCompression(PluginInfo plugin) throws IOException { + private ServerPlugin newInstalledPluginWithCompression(PluginInfo plugin) throws IOException { FileAndMd5 jar = new FileAndMd5(temp.newFile()); FileAndMd5 compressedJar = new FileAndMd5(temp.newFile()); - return new InstalledPlugin(plugin.setJarFile(jar.getFile()), jar, compressedJar); + return new ServerPlugin(plugin, PluginType.BUNDLED, null, jar, compressedJar, null); } @Test public void return_default_fields() throws Exception { - InstalledPlugin plugin = newInstalledPlugin(new PluginInfo("foo") + ServerPlugin plugin = newInstalledPlugin(new PluginInfo("foo") .setName("plugName") .setDescription("desc_it") .setVersion(Version.create("1.0")) @@ -166,14 +230,15 @@ public class InstalledActionTest { .setIssueTrackerUrl("issueTracker_url") .setImplementationBuild("sou_rev_sha1") .setSonarLintSupported(true)); - when(pluginFileSystem.getInstalledFiles()).thenReturn(singletonList(plugin)); + when(serverPluginRepository.getPlugins()).thenReturn(singletonList(plugin)); db.pluginDbTester().insertPlugin( p -> p.setKee(plugin.getPluginInfo().getKey()), + p -> p.setType(Type.valueOf(plugin.getType().name())), p -> p.setUpdatedAt(100L)); String response = tester.newRequest().execute().getInput(); - verifyZeroInteractions(updateCenterMatrixFactory); + verifyNoMoreInteractions(updateCenterMatrixFactory); assertJson(response).isSimilarTo( "{" + " \"plugins\":" + @@ -191,8 +256,8 @@ public class InstalledActionTest { " \"issueTrackerUrl\": \"issueTracker_url\"," + " \"implementationBuild\": \"sou_rev_sha1\"," + " \"sonarLintSupported\": true," + - " \"filename\": \"" + plugin.getLoadedJar().getFile().getName() + "\"," + - " \"hash\": \"" + plugin.getLoadedJar().getMd5() + "\"," + + " \"filename\": \"" + plugin.getJar().getFile().getName() + "\"," + + " \"hash\": \"" + plugin.getJar().getMd5() + "\"," + " \"updatedAt\": 100" + " }" + " ]" + @@ -201,7 +266,7 @@ public class InstalledActionTest { @Test public void return_compression_fields_if_available() throws Exception { - InstalledPlugin plugin = newInstalledPluginWithCompression(new PluginInfo("foo") + ServerPlugin plugin = newInstalledPluginWithCompression(new PluginInfo("foo") .setName("plugName") .setDescription("desc_it") .setVersion(Version.create("1.0")) @@ -213,10 +278,11 @@ public class InstalledActionTest { .setImplementationBuild("sou_rev_sha1") .setDocumentationPath("static/documentation.md") .setSonarLintSupported(true)); - when(pluginFileSystem.getInstalledFiles()).thenReturn(singletonList(plugin)); + when(serverPluginRepository.getPlugins()).thenReturn(singletonList(plugin)); db.pluginDbTester().insertPlugin( p -> p.setKee(plugin.getPluginInfo().getKey()), + p -> p.setType(Type.EXTERNAL), p -> p.setUpdatedAt(100L)); String response = tester.newRequest().execute().getInput(); @@ -240,8 +306,8 @@ public class InstalledActionTest { " \"implementationBuild\": \"sou_rev_sha1\"," + " \"sonarLintSupported\": true," + " \"documentationPath\": \"static/documentation.md\"," + - " \"filename\": \"" + plugin.getLoadedJar().getFile().getName() + "\"," + - " \"hash\": \"" + plugin.getLoadedJar().getMd5() + "\"," + + " \"filename\": \"" + plugin.getJar().getFile().getName() + "\"," + + " \"hash\": \"" + plugin.getJar().getMd5() + "\"," + " \"updatedAt\": 100" + " }" + " ]" + @@ -252,8 +318,8 @@ public class InstalledActionTest { public void category_is_returned_when_in_additional_fields() throws Exception { String jarFilename = getClass().getSimpleName() + "/" + "some.jar"; File jar = new File(getClass().getResource(jarFilename).toURI()); - when(pluginFileSystem.getInstalledFiles()).thenReturn(asList( - new InstalledPlugin(new PluginInfo("plugKey") + when(serverPluginRepository.getPlugins()).thenReturn(asList( + newInstalledPlugin(new PluginInfo("plugKey") .setName("plugName") .setDescription("desc_it") .setVersion(Version.create("1.0")) @@ -262,8 +328,8 @@ public class InstalledActionTest { .setOrganizationUrl("org_url") .setHomepageUrl("homepage_url") .setIssueTrackerUrl("issueTracker_url") - .setImplementationBuild("sou_rev_sha1") - .setJarFile(jar), new FileAndMd5(jar), null))); + .setImplementationBuild("sou_rev_sha1")))); + // .setJarFile(jar), new FileAndMd5(jar), null UpdateCenter updateCenter = mock(UpdateCenter.class); when(updateCenterMatrixFactory.getUpdateCenter(false)).thenReturn(Optional.of(updateCenter)); when(updateCenter.findAllCompatiblePlugins()).thenReturn( @@ -273,6 +339,7 @@ public class InstalledActionTest { db.pluginDbTester().insertPlugin( p -> p.setKee("plugKey"), + p -> p.setType(Type.EXTERNAL), p -> p.setFileHash("abcdplugKey"), p -> p.setUpdatedAt(111111L)); @@ -304,7 +371,7 @@ public class InstalledActionTest { @Test public void plugins_are_sorted_by_name_then_key_and_only_one_plugin_can_have_a_specific_name() throws IOException { - when(pluginFileSystem.getInstalledFiles()).thenReturn( + when(serverPluginRepository.getPlugins()).thenReturn( asList( plugin("A", "name2"), plugin("B", "name1"), @@ -313,18 +380,22 @@ public class InstalledActionTest { db.pluginDbTester().insertPlugin( p -> p.setKee("A"), + p -> p.setType(Type.EXTERNAL), p -> p.setFileHash("abcdA"), p -> p.setUpdatedAt(111111L)); db.pluginDbTester().insertPlugin( p -> p.setKee("B"), + p -> p.setType(Type.EXTERNAL), p -> p.setFileHash("abcdB"), p -> p.setUpdatedAt(222222L)); db.pluginDbTester().insertPlugin( p -> p.setKee("C"), + p -> p.setType(Type.EXTERNAL), p -> p.setFileHash("abcdC"), p -> p.setUpdatedAt(333333L)); db.pluginDbTester().insertPlugin( p -> p.setKee("D"), + p -> p.setType(Type.EXTERNAL), p -> p.setFileHash("abcdD"), p -> p.setUpdatedAt(444444L)); @@ -350,16 +421,19 @@ public class InstalledActionTest { String organization = random.nextBoolean() ? "SonarSource" : "SONARSOURCE"; String pluginKey = "plugKey"; File jar = new File(getClass().getResource(jarFilename).toURI()); - when(pluginFileSystem.getInstalledFiles()).thenReturn(asList( - new InstalledPlugin(new PluginInfo(pluginKey) + when(serverPluginRepository.getPlugins()).thenReturn(asList( + new ServerPlugin(new PluginInfo(pluginKey) .setName("plugName") .setVersion(Version.create("1.0")) .setLicense(license) .setOrganizationName(organization) - .setImplementationBuild("sou_rev_sha1") - .setJarFile(jar), new FileAndMd5(jar), null))); + .setImplementationBuild("sou_rev_sha1"), + PluginType.BUNDLED, + null, + new FileAndMd5(jar), new FileAndMd5(jar), null))); db.pluginDbTester().insertPlugin( p -> p.setKee(pluginKey), + p -> p.setType(Type.BUNDLED), p -> p.setFileHash("abcdplugKey"), p -> p.setUpdatedAt(111111L)); // ensure flag editionBundled is computed from jar info by enabling datacenter with other organization and license values @@ -392,13 +466,14 @@ public class InstalledActionTest { @Test public void only_one_plugin_can_have_a_specific_name_and_key() throws IOException { - when(pluginFileSystem.getInstalledFiles()).thenReturn( + when(serverPluginRepository.getPlugins()).thenReturn( asList( plugin("A", "name2"), plugin("A", "name2"))); db.pluginDbTester().insertPlugin( p -> p.setKee("A"), + p -> p.setType(Type.EXTERNAL), p -> p.setFileHash("abcdA"), p -> p.setUpdatedAt(111111L)); @@ -414,13 +489,13 @@ public class InstalledActionTest { assertThat(response).containsOnlyOnce("name2"); } - private InstalledPlugin plugin(String key, String name) throws IOException { + private ServerPlugin plugin(String key, String name) throws IOException { File file = temp.newFile(); PluginInfo info = new PluginInfo(key) .setName(name) .setVersion(Version.create("1.0")); info.setJarFile(file); - return new InstalledPlugin(info, new FileAndMd5(file), null); + return new ServerPlugin(info, PluginType.BUNDLED, null, new FileAndMd5(file), null, null); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PendingActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PendingActionTest.java index 91778eb05ed..a5653c14d0a 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PendingActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PendingActionTest.java @@ -19,9 +19,9 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -47,9 +47,6 @@ import static org.mockito.Mockito.when; import static org.sonar.test.JsonAssert.assertJson; public class PendingActionTest { - - private static final String DUMMY_CONTROLLER_KEY = "dummy"; - @Rule public UserSessionRule userSession = UserSessionRule.standalone(); @Rule @@ -105,7 +102,7 @@ public class PendingActionTest { @Test public void empty_arrays_are_returned_when_update_center_is_unavailable() { logInAsSystemAdministrator(); - when(updateCenterMatrixFactory.getUpdateCenter(false)).thenReturn(Optional.absent()); + when(updateCenterMatrixFactory.getUpdateCenter(false)).thenReturn(Optional.empty()); TestResponse response = tester.newRequest().execute(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PluginWSCommonsTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PluginWSCommonsTest.java index 1c01b48b1a1..d0a43431ed9 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PluginWSCommonsTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PluginWSCommonsTest.java @@ -27,8 +27,8 @@ import org.junit.rules.TemporaryFolder; import org.sonar.api.utils.text.JsonWriter; import org.sonar.core.platform.PluginInfo; import org.sonar.db.plugin.PluginDto; -import org.sonar.server.plugins.InstalledPlugin; -import org.sonar.server.plugins.InstalledPlugin.FileAndMd5; +import org.sonar.server.plugins.PluginFilesAndMd5.FileAndMd5; +import org.sonar.server.plugins.ServerPlugin; import org.sonar.server.ws.DumbResponse; import org.sonar.updatecenter.common.Plugin; import org.sonar.updatecenter.common.PluginUpdate; @@ -37,6 +37,7 @@ import org.sonar.updatecenter.common.Version; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.api.utils.DateUtils.parseDate; +import static org.sonar.server.plugins.PluginType.BUNDLED; import static org.sonar.server.plugins.ws.PluginWSCommons.toJSon; import static org.sonar.test.JsonAssert.assertJson; import static org.sonar.updatecenter.common.PluginUpdate.Status.COMPATIBLE; @@ -96,25 +97,25 @@ public class PluginWSCommonsTest { PluginDto dto = new PluginDto().setUpdatedAt(100L); FileAndMd5 loadedJar = new FileAndMd5(temp.newFile()); FileAndMd5 compressedJar = new FileAndMd5(temp.newFile()); - InstalledPlugin installedFile = new InstalledPlugin(gitPluginInfo(), loadedJar, compressedJar); + ServerPlugin serverPlugin = new ServerPlugin(gitPluginInfo(), BUNDLED, null, loadedJar, compressedJar, null); - PluginWSCommons.writePluginInfo(jsonWriter, gitPluginInfo(), null, dto, installedFile); + PluginWSCommons.writePluginInfo(jsonWriter, gitPluginInfo(), null, dto, serverPlugin); jsonWriter.close(); assertJson(response.outputAsString()).withStrictArrayOrder().isSimilarTo("{" + - " \"key\": \"scmgit\"," + - " \"name\": \"Git\"," + - " \"description\": \"Git SCM Provider.\"," + - " \"version\": \"1.0\"," + - " \"license\": \"GNU LGPL 3\"," + - " \"organizationName\": \"SonarSource\"," + - " \"organizationUrl\": \"http://www.sonarsource.com\"," + - " \"homepageUrl\": \"https://redirect.sonarsource.com/plugins/scmgit.html\"," + - " \"issueTrackerUrl\": \"http://jira.sonarsource.com/browse/SONARSCGIT\"," + - " \"sonarLintSupported\": true," + - " \"updatedAt\": 100," + - " \"filename\": \"" + loadedJar.getFile().getName() + "\"," + - " \"hash\": \"" + loadedJar.getMd5() + "\"" + + " \"key\": \"scmgit\"," + + " \"name\": \"Git\"," + + " \"description\": \"Git SCM Provider.\"," + + " \"version\": \"1.0\"," + + " \"license\": \"GNU LGPL 3\"," + + " \"organizationName\": \"SonarSource\"," + + " \"organizationUrl\": \"http://www.sonarsource.com\"," + + " \"homepageUrl\": \"https://redirect.sonarsource.com/plugins/scmgit.html\"," + + " \"issueTrackerUrl\": \"http://jira.sonarsource.com/browse/SONARSCGIT\"," + + " \"sonarLintSupported\": true," + + " \"updatedAt\": 100," + + " \"filename\": \"" + loadedJar.getFile().getName() + "\"," + + " \"hash\": \"" + loadedJar.getMd5() + "\"" + "}"); } @@ -193,6 +194,7 @@ public class PluginWSCommonsTest { " }" + "}"); } + @Test public void status_COMPATIBLE_is_displayed_COMPATIBLE_in_JSON() { assertThat(toJSon(COMPATIBLE)).isEqualTo("COMPATIBLE"); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UninstallActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UninstallActionTest.java index a11b034c594..8d4145fea2d 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UninstallActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UninstallActionTest.java @@ -19,61 +19,54 @@ */ package org.sonar.server.plugins.ws; -import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.sonar.api.server.ws.WebService; -import org.sonar.core.platform.PluginInfo; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.plugins.PluginUninstaller; -import org.sonar.server.plugins.ServerPluginRepository; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; @RunWith(DataProviderRunner.class) public class UninstallActionTest { - private static final String CONTROLLER_KEY = "api/plugins"; private static final String ACTION_KEY = "uninstall"; private static final String KEY_PARAM = "key"; private static final String PLUGIN_KEY = "findbugs"; @Rule public UserSessionRule userSessionRule = UserSessionRule.standalone(); - @Rule - public ExpectedException expectedException = ExpectedException.none(); - private ServerPluginRepository serverPluginRepository = mock(ServerPluginRepository.class); private PluginUninstaller pluginUninstaller = mock(PluginUninstaller.class); - private UninstallAction underTest = new UninstallAction(serverPluginRepository, pluginUninstaller, userSessionRule); + private UninstallAction underTest = new UninstallAction(pluginUninstaller, userSessionRule); private WsActionTester tester = new WsActionTester(underTest); @Test public void request_fails_with_ForbiddenException_when_user_is_not_logged_in() { - expectedException.expect(ForbiddenException.class); - expectedException.expectMessage("Insufficient privileges"); + TestRequest request = tester.newRequest(); - tester.newRequest().execute(); + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Insufficient privileges"); } @Test public void request_fails_with_ForbiddenException_when_user_is_not_system_administrator() { userSessionRule.logIn().setNonSystemAdministrator(); - expectedException.expect(ForbiddenException.class); - expectedException.expectMessage("Insufficient privileges"); + TestRequest request = tester.newRequest(); - tester.newRequest().execute(); + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Insufficient privileges"); } @Test @@ -95,53 +88,14 @@ public class UninstallActionTest { public void IAE_is_raised_when_key_param_is_not_provided() { logInAsSystemAdministrator(); - expectedException.expect(IllegalArgumentException.class); - - tester.newRequest().execute(); - } - - @Test - public void do_not_attempt_uninstall_if_no_plugin_in_repository_for_specified_key() { - logInAsSystemAdministrator(); - when(serverPluginRepository.getPluginInfo(PLUGIN_KEY)).thenReturn(null); - - tester.newRequest() - .setParam(KEY_PARAM, PLUGIN_KEY) - .execute(); - - verifyZeroInteractions(pluginUninstaller); - } - - @Test - @UseDataProvider("editionBundledOrganizationAndLicense") - public void IAE_is_raised_when_plugin_is_installed_and_is_edition_bundled(String organization, String license) { - logInAsSystemAdministrator(); - when(serverPluginRepository.getPluginInfo(PLUGIN_KEY)) - .thenReturn(new PluginInfo(PLUGIN_KEY) - .setOrganizationName(organization) - .setLicense(license)); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("SonarSource commercial plugin with key '" + PLUGIN_KEY + "' can only be uninstalled as part of a SonarSource edition"); - - tester.newRequest() - .setParam(KEY_PARAM, PLUGIN_KEY) - .execute(); - } - - @DataProvider - public static Object[][] editionBundledOrganizationAndLicense() { - return new Object[][] { - {"SonarSource", "SonarSource"}, - {"SonarSource", "Commercial"}, - {"sonarsource", "SOnArSOURCE"} - }; + TestRequest request = tester.newRequest(); + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class); } @Test - public void if_plugin_is_installed_uninstallation_is_triggered() { + public void uninstaller_is_called() { logInAsSystemAdministrator(); - when(serverPluginRepository.getPluginInfo(PLUGIN_KEY)).thenReturn(new PluginInfo(PLUGIN_KEY)); TestResponse response = tester.newRequest() .setParam(KEY_PARAM, PLUGIN_KEY) diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UpdateActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UpdateActionTest.java index f3501176c1b..a3954f67dd6 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UpdateActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UpdateActionTest.java @@ -19,8 +19,8 @@ */ package org.sonar.server.plugins.ws; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import java.util.Optional; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -124,7 +124,7 @@ public class UpdateActionTest { @Test public void IAE_is_raised_when_update_center_is_unavailable() { logInAsSystemAdministrator(); - when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent()); + when(updateCenterFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.empty()); expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("No plugin with key 'pluginKey'"); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/InstalledPluginsActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/PluginsActionTestFilesAndMD5.java similarity index 98% rename from server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/InstalledPluginsActionTest.java rename to server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/PluginsActionTestFilesAndMD5.java index e4e22e2ddcf..1f78451a190 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/InstalledPluginsActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/PluginsActionTestFilesAndMD5.java @@ -33,7 +33,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.test.JsonAssert.assertJson; -public class InstalledPluginsActionTest { +public class PluginsActionTestFilesAndMD5 { private ServerPluginRepository pluginRepository = mock(ServerPluginRepository.class); diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java index cfbb6f6d802..40361b6e4b1 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java @@ -22,8 +22,8 @@ package org.sonar.server.platform.platformlevel; import org.sonar.api.utils.Durations; import org.sonar.core.extension.CoreExtensionsInstaller; import org.sonar.core.platform.ComponentContainer; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.core.platform.PluginClassloaderFactory; -import org.sonar.core.platform.PluginLoader; import org.sonar.server.es.MigrationEsClientImpl; import org.sonar.server.l18n.ServerI18n; import org.sonar.server.platform.DatabaseServerCompatibility; @@ -41,8 +41,11 @@ import org.sonar.server.platform.db.migration.history.MigrationHistoryTableImpl; import org.sonar.server.platform.db.migration.version.DatabaseVersion; import org.sonar.server.platform.web.WebPagesCache; import org.sonar.server.plugins.InstalledPluginReferentialFactory; -import org.sonar.server.plugins.PluginFileSystem; +import org.sonar.server.plugins.PluginCompressor; +import org.sonar.server.plugins.PluginJarLoader; +import org.sonar.server.plugins.PluginUninstaller; import org.sonar.server.plugins.ServerPluginJarExploder; +import org.sonar.server.plugins.ServerPluginManager; import org.sonar.server.plugins.ServerPluginRepository; import org.sonar.server.plugins.WebServerExtensionInstaller; @@ -70,10 +73,12 @@ public class PlatformLevel2 extends PlatformLevel { WebPagesCache.class, // plugins + PluginJarLoader.class, ServerPluginRepository.class, + ServerPluginManager.class, ServerPluginJarExploder.class, - PluginLoader.class, - PluginFileSystem.class, + PluginClassLoader.class, + PluginCompressor.class, PluginClassloaderFactory.class, InstalledPluginReferentialFactory.class, WebServerExtensionInstaller.class, diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 79cf44d96c8..296a3d6f872 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -248,8 +248,8 @@ public class PlatformLevel4 extends PlatformLevel { ClusterVerification.class, LogServerId.class, LogOAuthWarning.class, - PluginDownloader.class, PluginUninstaller.class, + PluginDownloader.class, PageRepository.class, ResourceTypes.class, DefaultResourceTypes.get(), diff --git a/sonar-core/src/main/java/org/sonar/core/platform/ExplodedPlugin.java b/sonar-core/src/main/java/org/sonar/core/platform/ExplodedPlugin.java index 09d846aec04..bda09ba02c1 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/ExplodedPlugin.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/ExplodedPlugin.java @@ -23,12 +23,13 @@ import java.io.File; import java.util.Collection; public class ExplodedPlugin { - + private final PluginInfo pluginInfo; private final String key; private final File main; private final Collection libs; - public ExplodedPlugin(String key, File main, Collection libs) { + public ExplodedPlugin(PluginInfo pluginInfo, String key, File main, Collection libs) { + this.pluginInfo = pluginInfo; this.key = key; this.main = main; this.libs = libs; @@ -45,4 +46,8 @@ public class ExplodedPlugin { public Collection getLibs() { return libs; } + + public PluginInfo getPluginInfo() { + return pluginInfo; + } } diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java similarity index 82% rename from sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java rename to sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java index 5b997d9d407..503bc4dae0a 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java @@ -25,12 +25,13 @@ import java.io.Closeable; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import org.apache.commons.lang.SystemUtils; import org.sonar.api.Plugin; import org.sonar.api.utils.log.Loggers; import org.sonar.updatecenter.common.Version; -import static java.util.Arrays.asList; +import static java.util.Collections.singleton; /** * Loads the plugin JAR files by creating the appropriate classloaders and by instantiating @@ -46,22 +47,22 @@ import static java.util.Arrays.asList; *

    * This class is stateless. It does not keep pointers to classloaders and {@link org.sonar.api.Plugin}. */ -public class PluginLoader { - +public class PluginClassLoader { private static final String[] DEFAULT_SHARED_RESOURCES = {"org/sonar/plugins", "com/sonar/plugins", "com/sonarsource/plugins"}; - private static final Version COMPATIBILITY_MODE_MAX_VERSION = Version.create("5.2"); - private final PluginJarExploder jarExploder; private final PluginClassloaderFactory classloaderFactory; - public PluginLoader(PluginJarExploder jarExploder, PluginClassloaderFactory classloaderFactory) { - this.jarExploder = jarExploder; + public PluginClassLoader(PluginClassloaderFactory classloaderFactory) { this.classloaderFactory = classloaderFactory; } - public Map load(Map infoByKeys) { - Collection defs = defineClassloaders(infoByKeys); + public Map load(Collection plugins) { + return load(plugins.stream().collect(Collectors.toMap(ExplodedPlugin::getKey, x -> x))); + } + + public Map load(Map pluginsByKey) { + Collection defs = defineClassloaders(pluginsByKey); Map classloaders = classloaderFactory.create(defs); return instantiatePluginClasses(classloaders); } @@ -71,19 +72,19 @@ public class PluginLoader { * different than number of plugins. */ @VisibleForTesting - Collection defineClassloaders(Map infoByKeys) { + Collection defineClassloaders(Map pluginsByKey) { Map classloadersByBasePlugin = new HashMap<>(); - for (PluginInfo info : infoByKeys.values()) { - String baseKey = basePluginKey(info, infoByKeys); + for (ExplodedPlugin plugin : pluginsByKey.values()) { + PluginInfo info = plugin.getPluginInfo(); + String baseKey = basePluginKey(info, pluginsByKey); PluginClassLoaderDef def = classloadersByBasePlugin.get(baseKey); if (def == null) { def = new PluginClassLoaderDef(baseKey); classloadersByBasePlugin.put(baseKey, def); } - ExplodedPlugin explodedPlugin = jarExploder.explode(info); - def.addFiles(asList(explodedPlugin.getMain())); - def.addFiles(explodedPlugin.getLibs()); + def.addFiles(singleton(plugin.getMain())); + def.addFiles(plugin.getLibs()); def.addMainClass(info.getKey(), info.getMainClass()); for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) { @@ -115,8 +116,7 @@ public class PluginLoader { * @return the instances grouped by plugin key * @throws IllegalStateException if at least one plugin can't be correctly loaded */ - @VisibleForTesting - Map instantiatePluginClasses(Map classloaders) { + private static Map instantiatePluginClasses(Map classloaders) { // instantiate plugins Map instancesByPluginKey = new HashMap<>(); for (Map.Entry entry : classloaders.entrySet()) { @@ -128,13 +128,11 @@ public class PluginLoader { String pluginKey = mainClassEntry.getKey(); String mainClass = mainClassEntry.getValue(); try { - instancesByPluginKey.put(pluginKey, (Plugin) classLoader.loadClass(mainClass).newInstance()); + instancesByPluginKey.put(pluginKey, (Plugin) classLoader.loadClass(mainClass).getDeclaredConstructor().newInstance()); } catch (UnsupportedClassVersionError e) { - throw new IllegalStateException(String.format("The plugin [%s] does not support Java %s", - pluginKey, SystemUtils.JAVA_VERSION_TRIMMED), e); + throw new IllegalStateException(String.format("The plugin [%s] does not support Java %s", pluginKey, SystemUtils.JAVA_VERSION_TRIMMED), e); } catch (Throwable e) { - throw new IllegalStateException(String.format( - "Fail to instantiate class [%s] of plugin [%s]", mainClass, pluginKey), e); + throw new IllegalStateException(String.format("Fail to instantiate class [%s] of plugin [%s]", mainClass, pluginKey), e); } } } @@ -158,11 +156,11 @@ public class PluginLoader { * Get the root key of a tree of plugins. For example if plugin C depends on B, which depends on A, then * B and C must be attached to the classloader of A. The method returns A in the three cases. */ - static String basePluginKey(PluginInfo plugin, Map allPluginsPerKey) { + private static String basePluginKey(PluginInfo plugin, Map pluginsByKey) { String base = plugin.getKey(); String parentKey = plugin.getBasePlugin(); while (!Strings.isNullOrEmpty(parentKey)) { - PluginInfo parentPlugin = allPluginsPerKey.get(parentKey); + PluginInfo parentPlugin = pluginsByKey.get(parentKey).getPluginInfo(); base = parentPlugin.getKey(); parentKey = parentPlugin.getBasePlugin(); } diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginInfo.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginInfo.java index be62f1e8859..fe2a6e3145a 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/PluginInfo.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginInfo.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.jar.JarFile; @@ -48,57 +49,6 @@ public class PluginInfo implements Comparable { private static final Joiner SLASH_JOINER = Joiner.on(" / ").skipNulls(); - public static class RequiredPlugin { - - private static final Pattern PARSER = Pattern.compile("\\w+:.+"); - - private final String key; - private final Version minimalVersion; - - public RequiredPlugin(String key, Version minimalVersion) { - this.key = key; - this.minimalVersion = minimalVersion; - } - - public String getKey() { - return key; - } - - public Version getMinimalVersion() { - return minimalVersion; - } - - public static RequiredPlugin parse(String s) { - if (!PARSER.matcher(s).matches()) { - throw new IllegalArgumentException("Manifest field does not have correct format: " + s); - } - String[] fields = StringUtils.split(s, ':'); - return new RequiredPlugin(fields[0], Version.create(fields[1]).removeQualifier()); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RequiredPlugin that = (RequiredPlugin) o; - return key.equals(that.key); - } - - @Override - public int hashCode() { - return key.hashCode(); - } - - @Override - public String toString() { - return new StringBuilder().append(key).append(':').append(minimalVersion.getName()).toString(); - } - } - private final String key; private String name; @@ -385,18 +335,13 @@ public class PluginInfo implements Comparable { return false; } PluginInfo info = (PluginInfo) o; - if (!key.equals(info.key)) { - return false; - } - return !(version != null ? !version.equals(info.version) : info.version != null); + return Objects.equals(key, info.key) && Objects.equals(version, info.version); } @Override public int hashCode() { - int result = key.hashCode(); - result = 31 * result + (version != null ? version.hashCode() : 0); - return result; + return Objects.hash(key, version); } @Override @@ -419,41 +364,48 @@ public class PluginInfo implements Comparable { @VisibleForTesting static PluginInfo create(File jarFile, PluginManifest manifest) { + validateManifest(jarFile, manifest); + PluginInfo info = new PluginInfo(manifest.getKey()); + info.fillFields(jarFile, manifest); + return info; + } + + private static void validateManifest(File jarFile, PluginManifest manifest) { if (StringUtils.isBlank(manifest.getKey())) { throw MessageException.of(String.format("File is not a plugin. Please delete it and restart: %s", jarFile.getAbsolutePath())); } - PluginInfo info = new PluginInfo(manifest.getKey()); + } - info.setJarFile(jarFile); - info.setName(manifest.getName()); - info.setMainClass(manifest.getMainClass()); - info.setVersion(Version.create(manifest.getVersion())); - info.setDocumentationPath(getDocumentationPath(jarFile)); + protected void fillFields(File jarFile, PluginManifest manifest) { + setJarFile(jarFile); + setName(manifest.getName()); + setMainClass(manifest.getMainClass()); + setVersion(Version.create(manifest.getVersion())); + setDocumentationPath(getDocumentationPath(jarFile)); // optional fields - info.setDescription(manifest.getDescription()); - info.setLicense(manifest.getLicense()); - info.setOrganizationName(manifest.getOrganization()); - info.setOrganizationUrl(manifest.getOrganizationUrl()); - info.setDisplayVersion(manifest.getDisplayVersion()); + setDescription(manifest.getDescription()); + setLicense(manifest.getLicense()); + setOrganizationName(manifest.getOrganization()); + setOrganizationUrl(manifest.getOrganizationUrl()); + setDisplayVersion(manifest.getDisplayVersion()); String minSqVersion = manifest.getSonarVersion(); if (minSqVersion != null) { - info.setMinimalSqVersion(Version.create(minSqVersion)); + setMinimalSqVersion(Version.create(minSqVersion)); } - info.setHomepageUrl(manifest.getHomepage()); - info.setIssueTrackerUrl(manifest.getIssueTrackerUrl()); - info.setUseChildFirstClassLoader(manifest.isUseChildFirstClassLoader()); - info.setSonarLintSupported(manifest.isSonarLintSupported()); - info.setBasePlugin(manifest.getBasePlugin()); - info.setImplementationBuild(manifest.getImplementationBuild()); - String[] requiredPlugins = manifest.getRequirePlugins(); - if (requiredPlugins != null) { - Arrays.stream(requiredPlugins) + setHomepageUrl(manifest.getHomepage()); + setIssueTrackerUrl(manifest.getIssueTrackerUrl()); + setUseChildFirstClassLoader(manifest.isUseChildFirstClassLoader()); + setSonarLintSupported(manifest.isSonarLintSupported()); + setBasePlugin(manifest.getBasePlugin()); + setImplementationBuild(manifest.getImplementationBuild()); + String[] requiredPluginsFromManifest = manifest.getRequirePlugins(); + if (requiredPluginsFromManifest != null) { + Arrays.stream(requiredPluginsFromManifest) .map(RequiredPlugin::parse) .filter(t -> !"license".equals(t.key)) - .forEach(info::addRequiredPlugin); + .forEach(this::addRequiredPlugin); } - return info; } private static String getDocumentationPath(File file) { @@ -467,4 +419,54 @@ public class PluginInfo implements Comparable { return null; } + public static class RequiredPlugin { + + private static final Pattern PARSER = Pattern.compile("\\w+:.+"); + + private final String key; + private final Version minimalVersion; + + public RequiredPlugin(String key, Version minimalVersion) { + this.key = key; + this.minimalVersion = minimalVersion; + } + + public String getKey() { + return key; + } + + public Version getMinimalVersion() { + return minimalVersion; + } + + public static RequiredPlugin parse(String s) { + if (!PARSER.matcher(s).matches()) { + throw new IllegalArgumentException("Manifest field does not have correct format: " + s); + } + String[] fields = StringUtils.split(s, ':'); + return new RequiredPlugin(fields[0], Version.create(fields[1]).removeQualifier()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RequiredPlugin that = (RequiredPlugin) o; + return key.equals(that.key); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public String toString() { + return key + ':' + minimalVersion.getName(); + } + } } diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java index 508aa642666..7c97461c034 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java @@ -31,13 +31,13 @@ public abstract class PluginJarExploder { protected static final String LIB_RELATIVE_PATH_IN_JAR = "META-INF/lib"; - public abstract ExplodedPlugin explode(PluginInfo info); + public abstract ExplodedPlugin explode(PluginInfo plugin); protected Predicate newLibFilter() { return ze -> ze.getName().startsWith(LIB_RELATIVE_PATH_IN_JAR); } - protected ExplodedPlugin explodeFromUnzippedDir(String pluginKey, File jarFile, File unzippedDir) { + protected ExplodedPlugin explodeFromUnzippedDir(PluginInfo pluginInfo, File jarFile, File unzippedDir) { File libDir = new File(unzippedDir, PluginJarExploder.LIB_RELATIVE_PATH_IN_JAR); Collection libs; if (libDir.isDirectory() && libDir.exists()) { @@ -45,6 +45,6 @@ public abstract class PluginJarExploder { } else { libs = Collections.emptyList(); } - return new ExplodedPlugin(pluginKey, jarFile, libs); + return new ExplodedPlugin(pluginInfo, pluginInfo.getKey(), jarFile, libs); } } diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginRepository.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginRepository.java index c78a88f6d49..b49599f9c12 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/PluginRepository.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginRepository.java @@ -21,8 +21,8 @@ package org.sonar.core.platform; import java.util.Collection; import org.sonar.api.Plugin; -import org.sonar.api.scanner.ScannerSide; import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.scanner.ScannerSide; import org.sonar.api.server.ServerSide; /** @@ -45,5 +45,7 @@ public interface PluginRepository { */ Plugin getPluginInstance(String key); + Collection getPluginInstances(); + boolean hasPlugin(String key); } diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginClassLoaderTest.java similarity index 80% rename from sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java rename to sonar-core/src/test/java/org/sonar/core/platform/PluginClassLoaderTest.java index 1d12a4edab5..446bb257ae3 100644 --- a/sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java +++ b/sonar-core/src/test/java/org/sonar/core/platform/PluginClassLoaderTest.java @@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; -public class PluginLoaderTest { +public class PluginClassLoaderTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); @@ -46,25 +46,26 @@ public class PluginLoaderTest { public LogTester logTester = new LogTester(); private PluginClassloaderFactory classloaderFactory = mock(PluginClassloaderFactory.class); - private PluginLoader underTest = new PluginLoader(new FakePluginExploder(), classloaderFactory); + private PluginClassLoader underTest = new PluginClassLoader(classloaderFactory); @Test public void define_classloader() throws Exception { File jarFile = temp.newFile(); - PluginInfo info = new PluginInfo("foo") + PluginInfo plugin = new PluginInfo("foo") .setJarFile(jarFile) .setMainClass("org.foo.FooPlugin") .setMinimalSqVersion(Version.create("5.2")); - Collection defs = underTest.defineClassloaders(ImmutableMap.of("foo", info)); + ExplodedPlugin explodedPlugin = createExplodedPlugin(plugin); + Collection defs = underTest.defineClassloaders( + ImmutableMap.of("foo", explodedPlugin)); assertThat(defs).hasSize(1); PluginClassLoaderDef def = defs.iterator().next(); assertThat(def.getBasePluginKey()).isEqualTo("foo"); assertThat(def.isSelfFirstStrategy()).isFalse(); - assertThat(def.getFiles()).containsOnly(jarFile); + assertThat(def.getFiles()).containsAll(explodedPlugin.getLibs()); assertThat(def.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin")); - // TODO test mask - require change in sonar-classloader } /** @@ -95,19 +96,27 @@ public class PluginLoaderTest { .setBasePlugin("foo") .setUseChildFirstClassLoader(true); + ExplodedPlugin baseExplodedPlugin = createExplodedPlugin(base); + ExplodedPlugin extension1ExplodedPlugin = createExplodedPlugin(extension1); + ExplodedPlugin extension2ExplodedPlugin = createExplodedPlugin(extension2); Collection defs = underTest.defineClassloaders(ImmutableMap.of( - base.getKey(), base, extension1.getKey(), extension1, extension2.getKey(), extension2)); + base.getKey(), baseExplodedPlugin, + extension1.getKey(), extension1ExplodedPlugin, + extension2.getKey(), extension2ExplodedPlugin)); assertThat(defs).hasSize(1); PluginClassLoaderDef def = defs.iterator().next(); assertThat(def.getBasePluginKey()).isEqualTo("foo"); assertThat(def.isSelfFirstStrategy()).isFalse(); - assertThat(def.getFiles()).containsOnly(baseJarFile, extensionJar1, extensionJar2); + + assertThat(def.getFiles()) + .containsAll(baseExplodedPlugin.getLibs()) + .containsAll(extension1ExplodedPlugin.getLibs()) + .containsAll(extension2ExplodedPlugin.getLibs()); assertThat(def.getMainClassesByPluginKey()).containsOnly( entry("foo", "org.foo.FooPlugin"), entry("fooExtension1", "org.foo.Extension1Plugin"), entry("fooExtension2", "org.foo.Extension2Plugin")); - // TODO test mask - require change in sonar-classloader } @Test @@ -118,7 +127,8 @@ public class PluginLoaderTest { .setUseChildFirstClassLoader(true) .setMainClass("org.foo.FooPlugin"); - Collection defs = underTest.defineClassloaders(ImmutableMap.of("foo", info)); + Collection defs = underTest.defineClassloaders( + ImmutableMap.of("foo", createExplodedPlugin(info))); assertThat(defs).extracting(PluginClassLoaderDef::getBasePluginKey).containsExactly("foo"); List warnings = logTester.logs(LoggerLevel.WARN); @@ -133,20 +143,16 @@ public class PluginLoaderTest { .setMainClass("org.foo.FooPlugin") .setMinimalSqVersion(Version.create("4.5.2")); - Collection defs = underTest.defineClassloaders(ImmutableMap.of("foo", info)); + Collection defs = underTest.defineClassloaders( + ImmutableMap.of("foo", createExplodedPlugin(info))); assertThat(defs).extracting(PluginClassLoaderDef::getBasePluginKey).containsExactly("foo"); List warnings = logTester.logs(LoggerLevel.WARN); assertThat(warnings).contains("API compatibility mode is no longer supported. In case of error, plugin foo [foo] should package its dependencies."); } - /** - * Does not unzip jar file. It directly returns the JAR file defined on PluginInfo. - */ - private static class FakePluginExploder extends PluginJarExploder { - @Override - public ExplodedPlugin explode(PluginInfo info) { - return new ExplodedPlugin(info.getKey(), info.getNonNullJarFile(), Collections.emptyList()); - } + private ExplodedPlugin createExplodedPlugin(PluginInfo plugin) { + return new ExplodedPlugin(plugin, plugin.getKey(), new File(plugin.getKey() + ".jar"), Collections + .singleton(new File(plugin.getKey() + "-lib.jar"))); } } diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginInfoTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginInfoTest.java index ebc67bf175f..d85d614c848 100644 --- a/sonar-core/src/test/java/org/sonar/core/platform/PluginInfoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/platform/PluginInfoTest.java @@ -24,6 +24,7 @@ import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; @@ -31,7 +32,6 @@ import org.apache.commons.io.FileUtils; import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.sonar.api.utils.MessageException; @@ -41,6 +41,7 @@ import org.sonar.updatecenter.common.Version; import static com.google.common.collect.Ordering.natural; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.fail; @RunWith(DataProviderRunner.class) @@ -49,17 +50,14 @@ public class PluginInfoTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); - @Rule - public ExpectedException expectedException = ExpectedException.none(); - @Test public void test_RequiredPlugin() { PluginInfo.RequiredPlugin plugin = PluginInfo.RequiredPlugin.parse("java:1.1"); assertThat(plugin.getKey()).isEqualTo("java"); assertThat(plugin.getMinimalVersion().getName()).isEqualTo("1.1"); - assertThat(plugin.toString()).isEqualTo("java:1.1"); - assertThat(plugin.equals(PluginInfo.RequiredPlugin.parse("java:1.2"))).isTrue(); - assertThat(plugin.equals(PluginInfo.RequiredPlugin.parse("php:1.2"))).isFalse(); + assertThat(plugin).hasToString("java:1.1") + .isEqualTo(PluginInfo.RequiredPlugin.parse("java:1.2")) + .isNotEqualTo(PluginInfo.RequiredPlugin.parse("php:1.2")); try { PluginInfo.RequiredPlugin.parse("java"); @@ -210,7 +208,7 @@ public class PluginInfoTest { manifest.setOrganization("SonarSource"); manifest.setOrganizationUrl("http://sonarsource.com"); manifest.setIssueTrackerUrl("http://jira.com"); - manifest.setRequirePlugins(new String[]{"java:2.0", "pmd:1.3"}); + manifest.setRequirePlugins(new String[] {"java:2.0", "pmd:1.3"}); manifest.setSonarLintSupported(true); File jarFile = temp.newFile(); @@ -237,7 +235,7 @@ public class PluginInfoTest { manifest.setVersion("1.0"); manifest.setName("Java"); manifest.setMainClass("org.foo.FooPlugin"); - manifest.setRequirePlugins(new String[]{"license:" + version}); + manifest.setRequirePlugins(new String[] {"license:" + version}); File jarFile = temp.newFile(); PluginInfo pluginInfo = PluginInfo.create(jarFile, manifest); @@ -252,7 +250,7 @@ public class PluginInfoTest { manifest.setVersion("1.0"); manifest.setName("Java"); manifest.setMainClass("org.foo.FooPlugin"); - manifest.setRequirePlugins(new String[]{"java:2.0", "license:" + version, "pmd:1.3"}); + manifest.setRequirePlugins(new String[] {"java:2.0", "license:" + version, "pmd:1.3"}); File jarFile = temp.newFile(); PluginInfo pluginInfo = PluginInfo.create(jarFile, manifest); @@ -261,7 +259,7 @@ public class PluginInfoTest { @DataProvider public static Object[][] licenseVersions() { - return new Object[][]{ + return new Object[][] { {"0.3"}, {"7.2.0.1253"} }; @@ -292,7 +290,7 @@ public class PluginInfoTest { assertThat(pluginInfo.toString()).isEqualTo("[java / 1.1]"); pluginInfo.setImplementationBuild("SHA1"); - assertThat(pluginInfo.toString()).isEqualTo("[java / 1.1 / SHA1]"); + assertThat(pluginInfo).hasToString("[java / 1.1 / SHA1]"); } /** @@ -309,14 +307,13 @@ public class PluginInfoTest { public void fail_when_jar_is_not_a_plugin() throws IOException { // this JAR has a manifest but is not a plugin File jarRootDir = temp.newFolder(); - FileUtils.write(new File(jarRootDir, "META-INF/MANIFEST.MF"), "Build-Jdk: 1.6.0_15"); + FileUtils.write(new File(jarRootDir, "META-INF/MANIFEST.MF"), "Build-Jdk: 1.6.0_15", StandardCharsets.UTF_8); File jar = temp.newFile(); ZipUtils.zipDir(jarRootDir, jar); - expectedException.expect(MessageException.class); - expectedException.expectMessage("File is not a plugin. Please delete it and restart: " + jar.getAbsolutePath()); - - PluginInfo.create(jar); + assertThatThrownBy(() -> PluginInfo.create(jar)) + .isInstanceOf(MessageException.class) + .hasMessage("File is not a plugin. Please delete it and restart: " + jar.getAbsolutePath()); } PluginInfo withMinSqVersion(@Nullable String version) { diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java index 4e103b1d349..59824693472 100644 --- a/sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java +++ b/sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java @@ -44,7 +44,7 @@ public class PluginJarExploderTest { public ExplodedPlugin explode(PluginInfo info) { try { ZipUtils.unzip(jarFile, toDir, newLibFilter()); - return explodeFromUnzippedDir(info.getKey(), info.getNonNullJarFile(), toDir); + return explodeFromUnzippedDir(info, info.getNonNullJarFile(), toDir); } catch (Exception e) { throw new IllegalStateException(e); } @@ -65,7 +65,7 @@ public class PluginJarExploderTest { PluginJarExploder exploder = new PluginJarExploder() { @Override public ExplodedPlugin explode(PluginInfo info) { - return explodeFromUnzippedDir("foo", info.getNonNullJarFile(), toDir); + return explodeFromUnzippedDir(pluginInfo, info.getNonNullJarFile(), toDir); } }; ExplodedPlugin exploded = exploder.explode(pluginInfo); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java index 6a7f5c663ff..9dfd4d3367d 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java @@ -42,7 +42,7 @@ import org.sonar.core.extension.CoreExtensionsLoader; import org.sonar.core.platform.ComponentContainer; import org.sonar.core.platform.PluginClassloaderFactory; import org.sonar.core.platform.PluginInfo; -import org.sonar.core.platform.PluginLoader; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.core.platform.PluginRepository; import org.sonar.core.util.DefaultHttpDownloader; import org.sonar.core.util.UuidFactoryImpl; @@ -99,7 +99,7 @@ public class GlobalContainer extends ComponentContainer { add( // plugins ScannerPluginRepository.class, - PluginLoader.class, + PluginClassLoader.class, PluginClassloaderFactory.class, ScannerPluginJarExploder.class, ExtensionInstaller.class, diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginJarExploder.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginJarExploder.java index c99ab74217a..107d5311268 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginJarExploder.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginJarExploder.java @@ -42,7 +42,7 @@ public class ScannerPluginJarExploder extends PluginJarExploder { public ExplodedPlugin explode(PluginInfo info) { try { File dir = unzipFile(info.getNonNullJarFile()); - return explodeFromUnzippedDir(info.getKey(), info.getNonNullJarFile(), dir); + return explodeFromUnzippedDir(info, info.getNonNullJarFile(), dir); } catch (Exception e) { throw new IllegalStateException(String.format("Fail to open plugin [%s]: %s", info.getKey(), info.getNonNullJarFile().getAbsolutePath()), e); } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java index 801e3b93ad0..2d82aee77a0 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java @@ -22,18 +22,19 @@ package org.sonar.scanner.bootstrap; import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.function.Function; +import java.util.stream.Collectors; import javax.annotation.CheckForNull; import org.picocontainer.Startable; import org.sonar.api.Plugin; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; +import org.sonar.core.platform.ExplodedPlugin; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.core.platform.PluginInfo; -import org.sonar.core.platform.PluginLoader; +import org.sonar.core.platform.PluginJarExploder; import org.sonar.core.platform.PluginRepository; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; import static org.sonar.api.utils.Preconditions.checkState; /** @@ -43,24 +44,25 @@ public class ScannerPluginRepository implements PluginRepository, Startable { private static final Logger LOG = Loggers.get(ScannerPluginRepository.class); private final PluginInstaller installer; - private final PluginLoader loader; + private final PluginJarExploder pluginJarExploder; + private final PluginClassLoader loader; private Map pluginInstancesByKeys; private Map pluginsByKeys; private Map keysByClassLoader; - public ScannerPluginRepository(PluginInstaller installer, PluginLoader loader) { + public ScannerPluginRepository(PluginInstaller installer, PluginJarExploder pluginJarExploder, PluginClassLoader loader) { this.installer = installer; + this.pluginJarExploder = pluginJarExploder; this.loader = loader; } @Override public void start() { pluginsByKeys = new HashMap<>(installer.installRemotes()); - pluginInstancesByKeys = new HashMap<>( - loader.load(pluginsByKeys.values().stream() - .map(ScannerPlugin::getInfo) - .collect(toMap(PluginInfo::getKey, Function.identity())))); + Map explodedPLuginsByKey = pluginsByKeys.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> pluginJarExploder.explode(e.getValue().getInfo()))); + pluginInstancesByKeys = new HashMap<>(loader.load(explodedPLuginsByKey)); // this part is only used by medium tests for (Object[] localPlugin : installer.installLocals()) { @@ -127,6 +129,11 @@ public class ScannerPluginRepository implements PluginRepository, Startable { return instance; } + @Override + public Collection getPluginInstances() { + return pluginInstancesByKeys.values(); + } + @Override public boolean hasPlugin(String key) { return pluginsByKeys.containsKey(key); diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginRepositoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginRepositoryTest.java index e316868c831..268a8403d7a 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginRepositoryTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginRepositoryTest.java @@ -20,10 +20,14 @@ package org.sonar.scanner.bootstrap; import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.util.Collections; import org.junit.Test; import org.sonar.api.Plugin; +import org.sonar.core.platform.ExplodedPlugin; +import org.sonar.core.platform.PluginClassLoader; import org.sonar.core.platform.PluginInfo; -import org.sonar.core.platform.PluginLoader; +import org.sonar.core.platform.PluginJarExploder; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; @@ -36,8 +40,9 @@ import static org.mockito.Mockito.when; public class ScannerPluginRepositoryTest { PluginInstaller installer = mock(PluginInstaller.class); - PluginLoader loader = mock(PluginLoader.class); - ScannerPluginRepository underTest = new ScannerPluginRepository(installer, loader); + PluginClassLoader loader = mock(PluginClassLoader.class); + PluginJarExploder exploder = new FakePluginJarExploder(); + ScannerPluginRepository underTest = new ScannerPluginRepository(installer, exploder, loader); @Test public void install_and_load_plugins() { @@ -53,6 +58,7 @@ public class ScannerPluginRepositoryTest { assertThat(underTest.getPluginsByKey()).isEqualTo(plugins); assertThat(underTest.getPluginInfo("squid")).isSameAs(info); assertThat(underTest.getPluginInstance("squid")).isSameAs(instance); + assertThat(underTest.getPluginInstances()).containsOnly(instance); underTest.stop(); verify(loader).unload(anyCollection()); @@ -75,4 +81,13 @@ public class ScannerPluginRepositoryTest { assertThat(e).hasMessage("Plugin [unknown] does not exist"); } } + + private static class FakePluginJarExploder extends PluginJarExploder { + @Override + public ExplodedPlugin explode(PluginInfo plugin) { + return new ExplodedPlugin(plugin, plugin.getKey(), new File(plugin.getKey() + ".jar"), Collections + .singleton(new File(plugin.getKey() + "-lib.jar"))); + } + + } } -- 2.39.5