]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13643 Save plugins with type
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Wed, 26 Aug 2020 23:30:15 +0000 (18:30 -0500)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Sep 2020 20:07:13 +0000 (20:07 +0000)
91 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStep.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadReportAnalysisMetadataHolderStepTest.java
server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginJarExploder.java
server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/CePluginRepositoryTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/plugin/PluginDto.java
server/sonar-db-dao/src/main/resources/org/sonar/db/plugin/PluginMapper.xml
server/sonar-db-dao/src/schema/schema-sq.ddl
server/sonar-db-dao/src/test/java/org/sonar/db/plugin/PluginDaoTest.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPlugins.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullable.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/DbVersion85.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPlugins.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddTypeToPluginsTest/schema.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AlterTypeInPluginNotNullableTest/schema.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/PopulateTypeInPluginsTest/schema.sql [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java [deleted file]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginCompressor.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java [deleted file]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFilesAndMd5.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginType.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPlugin.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginInfo.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginManager.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginCompressorTest.java [new file with mode: 0644]
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java [deleted file]
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFilesAndMd5Test.java [new file with mode: 0644]
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginJarLoaderTest.java [new file with mode: 0644]
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginInfoTest.java [new file with mode: 0644]
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginManagerTest.java [new file with mode: 0644]
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java
server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java
server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java
server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java
server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java
server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/UpgradesAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/AvailableAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/DownloadAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstallAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/InstalledAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PendingAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/PluginWSCommons.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UninstallAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/plugins/ws/UpdatesAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/language/LanguageValidationTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/UpgradesActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AbstractUpdateCenterBasedPluginsWsActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/AvailableActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/DownloadActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstallActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/InstalledActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PendingActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/PluginWSCommonsTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UninstallActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/plugins/ws/UpdateActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/InstalledPluginsActionTest.java [deleted file]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/PluginsActionTestFilesAndMD5.java [new file with mode: 0644]
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
sonar-core/src/main/java/org/sonar/core/platform/ExplodedPlugin.java
sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/platform/PluginInfo.java
sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java
sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java [deleted file]
sonar-core/src/main/java/org/sonar/core/platform/PluginRepository.java
sonar-core/src/test/java/org/sonar/core/platform/PluginClassLoaderTest.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/platform/PluginInfoTest.java
sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java
sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java [deleted file]
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginJarExploder.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginRepository.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginRepositoryTest.java

index 9fe44ff27966282eb6628cd0774a80ffbb63955e..582475e04dadb307467d64752b6573f7b1341bc5 100644 (file)
@@ -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<String> profileKeys = metadata.getQprofilesPerLanguage().values().stream()
+    List<String> 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<QProfileDto> profiles = dbClient.qualityProfileDao().selectByUuids(dbSession, profileKeys);
       String badKeys = profiles.stream()
index 17d681d58e95af907bb204913bb32b3c5f2f50f5..4d10423cfffb5bce6c9e12b4e7289da81027dd28 100644 (file)
@@ -486,6 +486,11 @@ public class LoadReportAnalysisMetadataHolderStepTest {
       throw new UnsupportedOperationException();
     }
 
+    @Override
+    public Collection<Plugin> getPluginInstances() {
+      throw new UnsupportedOperationException();
+    }
+
     @Override
     public boolean hasPlugin(String key) {
       return pluginsMap.containsKey(key);
index 18959e76b31c121377c0eb703b3b003100de62e1..b5d95a6f04ca1fceae375161bb6cd5011efb4521 100644 (file)
@@ -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);
index 4c9476647ce1c118edcabbfbcb6ed7a02e941baf..cc85465e78bd673741018fdb2c7f094cc1420ddc 100644 (file)
@@ -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<String, PluginInfo> pluginInfosByKeys = new HashMap<>();
   private final Map<String, Plugin> 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<String, ExplodedPlugin> explodedPluginsByKey = extractPlugins(pluginInfosByKeys);
+    pluginInstancesByKeys.putAll(loader.load(explodedPluginsByKey));
     started.set(true);
   }
 
@@ -77,6 +82,12 @@ public class CePluginRepository implements PluginRepository, Startable {
     }
   }
 
+  private Map<String, ExplodedPlugin> extractPlugins(Map<String, PluginInfo> 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<Plugin> getPluginInstances() {
+    return pluginInstancesByKeys.values();
+  }
+
   @Override
   public boolean hasPlugin(String key) {
     checkState(started.get(), NOT_STARTED_YET);
index f7e3793a81815513a53d6958ed27a2c4d7320042..7d57bbfcb7294c017723498166bdc185fd24fe06 100644 (file)
@@ -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,
index 047eebf6eade80adc966884bb9f94fb4051cd79e..4e3c93dd164cb84eb2dc6a3a66f1873bc84530fa 100644 (file)
 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<String, Plugin> load(Map<String, PluginInfo> infoByKeys) {
+    public Map<String, Plugin> load(Map<String, ExplodedPlugin> infoByKeys) {
       Map<String, Plugin> result = new HashMap<>();
       for (String pluginKey : infoByKeys.keySet()) {
         result.put(pluginKey, mock(Plugin.class));
index 6b3169ab06f642019e5f4572e569ffb463574b70..375d4e847f7e083450fc1e15f1d9ccceb4b0ca66 100644 (file)
@@ -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)
index 0ff5f6cf543a550e02bfe5bfb25de906e7284547..ce585f75d673496e51e5d0961bead026ec8575fe 100644 (file)
@@ -9,6 +9,7 @@
     kee,
     base_plugin_key as basePluginKey,
     file_hash as fileHash,
+    type,
     created_at as createdAt,
     updated_at as updatedAt
   </sql>
@@ -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}
index 9345fa56fba341a54f1bd2dc9c64cf2558fcb53e..10ff174c94dd2bad531821ad83fb225aa193bfbc 100644 (file)
@@ -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");
index 40492b676dbdbf84a196a175d515c8ce05554fa1..65d1abf398018c40e4a957060a10186b8ed4602e 100644 (file)
  */
 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<PluginDto> 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<PluginDto> 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 (file)
index 0000000..13c2f61
--- /dev/null
@@ -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 (file)
index 0000000..c8e7634
--- /dev/null
@@ -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());
+  }
+}
index cf4917d8abe31801165706a4f69f09217a25a6a2..d6bb98a5bdb49dec0577a51da59d5a06ebcd7401 100644 (file)
@@ -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 (file)
index 0000000..33a3f73
--- /dev/null
@@ -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 (file)
index 0000000..243bcd5
--- /dev/null
@@ -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 (file)
index 0000000..c08e6f8
--- /dev/null
@@ -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 (file)
index 0000000..7fe7241
--- /dev/null
@@ -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 (file)
index 0000000..c2a001f
--- /dev/null
@@ -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 (file)
index 0000000..3d438e8
--- /dev/null
@@ -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 (file)
index 0000000..3d438e8
--- /dev/null
@@ -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");
index 952b36208f0878a43c1b51fbcabb2459c5dd7c96..2496fd0a186055db005b56d4e4ce26ec40223164 100644 (file)
@@ -150,6 +150,11 @@ public class ServerExtensionInstallerTest {
       return pluginsMap.get(key);
     }
 
+    @Override
+    public Collection<Plugin> 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/InstalledPlugin.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java
deleted file mode 100644 (file)
index f9d8993..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.io.InputStream;
-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;
-  private final FileAndMd5 loadedJar;
-  @Nullable
-  private final FileAndMd5 compressedJar;
-
-  public InstalledPlugin(PluginInfo plugin, FileAndMd5 loadedJar, @Nullable FileAndMd5 compressedJar) {
-    this.plugin = requireNonNull(plugin);
-    this.loadedJar = requireNonNull(loadedJar);
-    this.compressedJar = compressedJar;
-  }
-
-  public PluginInfo getPluginInfo() {
-    return plugin;
-  }
-
-  public FileAndMd5 getLoadedJar() {
-    return loadedJar;
-  }
-
-  @Nullable
-  public FileAndMd5 getCompressedJar() {
-    return compressedJar;
-  }
-
-  @Immutable
-  public static final class FileAndMd5 {
-    private final File file;
-    private final String md5;
-
-    public FileAndMd5(File file) {
-      try (InputStream fis = FileUtils.openInputStream(file)) {
-        this.file = file;
-        this.md5 = DigestUtils.md5Hex(fis);
-      } catch (IOException e) {
-        throw new IllegalStateException("Fail to compute md5 of " + file, e);
-      }
-    }
-
-    public File getFile() {
-      return file;
-    }
-
-    public String getMd5() {
-      return md5;
-    }
-  }
-}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginCompressor.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginCompressor.java
new file mode 100644 (file)
index 0000000..faa2551
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * 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.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.jar.JarInputStream;
+import java.util.jar.Pack200;
+import java.util.zip.GZIPOutputStream;
+import org.sonar.api.config.Configuration;
+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.server.plugins.PluginFilesAndMd5.FileAndMd5;
+
+@ServerSide
+public class PluginCompressor {
+
+  public static final String PROPERTY_PLUGIN_COMPRESSION_ENABLE = "sonar.pluginsCompression.enable";
+  private static final Logger LOG = Loggers.get(PluginCompressor.class);
+
+  private final Configuration configuration;
+
+  public PluginCompressor(Configuration configuration) {
+    this.configuration = configuration;
+  }
+
+  public boolean enabled() {
+    return configuration.getBoolean(PROPERTY_PLUGIN_COMPRESSION_ENABLE).orElse(false);
+  }
+
+  /**
+   * @param loadedJar the JAR loaded by classloaders. It differs from {@code jar}
+   *                  which is the initial location of JAR as seen by users
+   */
+  public PluginFilesAndMd5 compress(String key,  File jar, File loadedJar) {
+    Optional<File> compressed = compressJar(key, jar, loadedJar);
+    return new PluginFilesAndMd5(new FileAndMd5(loadedJar), compressed.map(FileAndMd5::new).orElse(null));
+  }
+
+  private Optional<File> compressJar(String key,  File jar, File loadedJar) {
+    if (!configuration.getBoolean(PROPERTY_PLUGIN_COMPRESSION_ENABLE).orElse(false)) {
+      return Optional.empty();
+    }
+
+    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()) {
+      try {
+        LOG.debug("Found pack200: " + sourcePack200Path);
+        Files.copy(sourcePack200Path, targetPack200);
+      } catch (IOException e) {
+        throw new IllegalStateException("Failed to copy pack200 file from " + sourcePack200Path + " to " + targetPack200, e);
+      }
+    } else {
+      pack200(loadedJar.toPath(), targetPack200, key);
+    }
+    return Optional.of(targetPack200.toFile());
+  }
+
+  private static void pack200(Path jarPath, Path toPack200Path, String pluginKey) {
+    Profiler profiler = Profiler.create(LOG);
+    profiler.startInfo("Compressing plugin " + pluginKey + " [pack200]");
+
+    try (JarInputStream in = new JarInputStream(new BufferedInputStream(Files.newInputStream(jarPath)));
+      OutputStream out = new GZIPOutputStream(new BufferedOutputStream(Files.newOutputStream(toPack200Path)))) {
+      Pack200.newPacker().pack(in, out);
+    } catch (IOException e) {
+      throw new IllegalStateException(String.format("Fail to pack200 plugin [%s] '%s' to '%s'", pluginKey, jarPath, toPack200Path), e);
+    }
+    profiler.stopInfo();
+  }
+
+  private static Path getPack200Path(Path jar) {
+    String jarFileName = jar.getFileName().toString();
+    String filename = jarFileName.substring(0, jarFileName.length() - 3) + "pack.gz";
+    return jar.resolveSibling(filename);
+  }
+}
index 4c9a9e68fa331579a42bfb9e756087d6b9bcfcb3..4883c17f69bceae25a7f5101b2c6ced4316c88a0 100644 (file)
  */
 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<String> getDownloadedPluginFilenames() {
-    List<String> 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/PluginFileSystem.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java
deleted file mode 100644 (file)
index 8dd2162..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * 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.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-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;
-import java.util.zip.GZIPOutputStream;
-import org.sonar.api.config.Configuration;
-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;
-
-@ServerSide
-public class PluginFileSystem {
-
-  public static final String PROPERTY_PLUGIN_COMPRESSION_ENABLE = "sonar.pluginsCompression.enable";
-  private static final Logger LOG = Loggers.get(PluginFileSystem.class);
-
-  private final Configuration configuration;
-  private final Map<String, InstalledPlugin> installedFiles = new HashMap<>();
-
-  public PluginFileSystem(Configuration configuration) {
-    this.configuration = configuration;
-  }
-
-  /**
-   * @param plugin
-   * @param loadedJar the JAR loaded by classloaders. It differs from {@code plugin.getJarFile()}
-   *                  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<File> 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<InstalledPlugin> getInstalledPlugin(String pluginKey) {
-    return Optional.ofNullable(installedFiles.get(pluginKey));
-  }
-
-  public Collection<InstalledPlugin> getInstalledFiles() {
-    return installedFiles.values();
-  }
-
-  private Optional<File> compressJar(PluginInfo plugin, File jar) {
-    if (!configuration.getBoolean(PROPERTY_PLUGIN_COMPRESSION_ENABLE).orElse(false)) {
-      return Optional.empty();
-    }
-
-    Path targetPack200 = getPack200Path(jar.toPath());
-    Path sourcePack200Path = getPack200Path(plugin.getNonNullJarFile().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()) {
-      try {
-        LOG.debug("Found pack200: " + sourcePack200Path);
-        Files.copy(sourcePack200Path, targetPack200);
-      } catch (IOException e) {
-        throw new IllegalStateException("Failed to copy pack200 file from " + sourcePack200Path + " to " + targetPack200, e);
-      }
-    } else {
-      pack200(jar.toPath(), targetPack200, plugin.getKey());
-    }
-    return Optional.of(targetPack200.toFile());
-  }
-
-  private static void pack200(Path jarPath, Path toPack200Path, String pluginKey) {
-    Profiler profiler = Profiler.create(LOG);
-    profiler.startInfo("Compressing plugin " + pluginKey + " [pack200]");
-
-    try (JarInputStream in = new JarInputStream(new BufferedInputStream(Files.newInputStream(jarPath)));
-      OutputStream out = new GZIPOutputStream(new BufferedOutputStream(Files.newOutputStream(toPack200Path)))) {
-      Pack200.newPacker().pack(in, out);
-    } catch (IOException e) {
-      throw new IllegalStateException(String.format("Fail to pack200 plugin [%s] '%s' to '%s'", pluginKey, jarPath, toPack200Path), e);
-    }
-    profiler.stopInfo();
-  }
-
-  private static Path getPack200Path(Path jar) {
-    String jarFileName = jar.getFileName().toString();
-    String filename = jarFileName.substring(0, jarFileName.length() - 3) + "pack.gz";
-    return jar.resolveSibling(filename);
-  }
-}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFilesAndMd5.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFilesAndMd5.java
new file mode 100644 (file)
index 0000000..338f1a2
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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.io.InputStream;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FileUtils;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class PluginFilesAndMd5 {
+  private final FileAndMd5 loadedJar;
+  @Nullable
+  private final FileAndMd5 compressedJar;
+
+  public PluginFilesAndMd5(FileAndMd5 loadedJar, @Nullable FileAndMd5 compressedJar) {
+    this.loadedJar = requireNonNull(loadedJar);
+    this.compressedJar = compressedJar;
+  }
+
+  public FileAndMd5 getLoadedJar() {
+    return loadedJar;
+  }
+
+  @Nullable
+  public FileAndMd5 getCompressedJar() {
+    return compressedJar;
+  }
+
+  @Immutable
+  public static class FileAndMd5 {
+    private final File file;
+    private final String md5;
+
+    public FileAndMd5(File file) {
+      try (InputStream fis = FileUtils.openInputStream(file)) {
+        this.file = file;
+        this.md5 = DigestUtils.md5Hex(fis);
+      } catch (IOException e) {
+        throw new IllegalStateException("Fail to compute md5 of " + file, e);
+      }
+    }
+
+    public File getFile() {
+      return file;
+    }
+
+    public String getMd5() {
+      return 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 (file)
index 0000000..e604d9c
--- /dev/null
@@ -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<String> DEFAULT_BLACKLISTED_PLUGINS = ImmutableSet.of("scmactivity", "issuesreport", "genericcoverage");
+  // List of plugins that should prevent the server to finish its startup
+  private static final Set<String> FORBIDDEN_COMPATIBLE_PLUGINS = ImmutableSet.of("sqale", "report", "views");
+
+  private final ServerFileSystem fs;
+  private final SonarRuntime runtime;
+  private final Set<String> blacklistedPluginKeys;
+
+  public PluginJarLoader(ServerFileSystem fs, SonarRuntime runtime) {
+    this(fs, runtime, DEFAULT_BLACKLISTED_PLUGINS);
+  }
+
+  PluginJarLoader(ServerFileSystem fs, SonarRuntime runtime, Set<String> 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<ServerPluginInfo> loadPlugins() {
+    Map<String, ServerPluginInfo> 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<String, ServerPluginInfo> 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<String, ServerPluginInfo> 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<String, ServerPluginInfo> 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<String> 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<String, ServerPluginInfo> 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<String, ? extends PluginInfo> map, PluginInfo value, Function<PluginInfo, RuntimeException> msg) {
+    PluginInfo pluginInfo = map.get(value.getKey());
+    if (pluginInfo != null) {
+      throw msg.apply(pluginInfo);
+    }
+  }
+
+  private List<ServerPluginInfo> getBundledPluginsMetadata() {
+    return loadPluginsFromDir(fs.getInstalledBundledPluginsDir(), jar -> ServerPluginInfo.create(jar, BUNDLED));
+  }
+
+  private List<ServerPluginInfo> getExternalPluginsMetadata() {
+    return loadPluginsFromDir(fs.getInstalledExternalPluginsDir(), jar -> ServerPluginInfo.create(jar, EXTERNAL));
+  }
+
+  private List<PluginInfo> 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 <T extends PluginInfo> List<T> loadPluginsFromDir(File pluginsDir, Function<File, T> 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<File> 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 (file)
index 0000000..0621c10
--- /dev/null
@@ -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
+}
index f734a346440450ce3fb34dc61d6b146d1ca2f582..5c1e7c8820367b26c05335c59078cf90954fe855 100644 (file)
@@ -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<File> 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<String> 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<PluginInfo> 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<File> 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<String> 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 (file)
index 0000000..8bffd9d
--- /dev/null
@@ -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 (file)
index 0000000..0469602
--- /dev/null
@@ -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);
+  }
+}
index 481251fb121ba449f0e27921434d2db4eba83960..5612fdaa0e143e49855dc5bf8844940475cf054a 100644 (file)
@@ -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 (file)
index 0000000..43f90ba
--- /dev/null
@@ -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
+ * <ul>
+ *   <li>installation of new plugins (effective after server startup)</li>
+ *   <li>un-installation of plugins (effective after server startup)</li>
+ *   <li>cancel pending installations/un-installations</li>
+ *   <li>instantiation of plugin entry-points</li>
+ * </ul>
+ */
+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<ServerPluginInfo> loadedPlugins = pluginJarLoader.loadPlugins();
+    logInstalledPlugins(loadedPlugins);
+    Collection<ExplodedPlugin> explodedPlugins = extractPlugins(loadedPlugins);
+    Map<String, Plugin> instancesByKey = pluginClassLoader.load(explodedPlugins);
+    Map<String, PluginType> typesByKey = getTypesByKey(loadedPlugins);
+    List<ServerPlugin> plugins = compressAndCreateServerPlugins(explodedPlugins, instancesByKey, typesByKey);
+    pluginRepository.addPlugins(plugins);
+  }
+
+  private static Map<String, PluginType> getTypesByKey(Collection<ServerPluginInfo> loadedPlugins) {
+    return loadedPlugins.stream().collect(Collectors.toMap(ServerPluginInfo::getKey, ServerPluginInfo::getType));
+  }
+
+  @Override
+  public void stop() {
+    pluginClassLoader.unload(pluginRepository.getPluginInstances());
+  }
+
+  private static void logInstalledPlugins(Collection<ServerPluginInfo> plugins) {
+    plugins.stream().sorted().forEach(plugin -> LOG.info("Deploy plugin {} / {} / {}", plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild()));
+  }
+
+  private Collection<ExplodedPlugin> extractPlugins(Collection<ServerPluginInfo> plugins) {
+    return plugins.stream().map(pluginJarExploder::explode).collect(Collectors.toList());
+  }
+
+  private List<ServerPlugin> compressAndCreateServerPlugins(Collection<ExplodedPlugin> explodedPlugins, Map<String, Plugin> instancesByKey, Map<String, PluginType> typseByKey) {
+    List<ServerPlugin> 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;
+  }
+}
index 33b03519040fb861d1db53410a93cf6ce5c789e9..2b2863f8a3c923343e574c92c7d0d0b7d63476e2 100644 (file)
  */
 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
- * <ul>
- *   <li>installation of new plugins (effective after server startup)</li>
- *   <li>un-installation of plugins (effective after server startup)</li>
- *   <li>cancel pending installations/un-installations</li>
- *   <li>instantiation of plugin entry-points</li>
- * </ul>
- */
-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<String> DEFAULT_BLACKLISTED_PLUGINS = ImmutableSet.of("scmactivity", "issuesreport", "genericcoverage");
-  // List of plugins that should prevent the server to finish its startup
-  private static final Set<String> 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<String> blacklistedPluginKeys = DEFAULT_BLACKLISTED_PLUGINS;
-
-  // following fields are available after startup
-  private final Map<String, PluginInfo> pluginInfosByKeys = new HashMap<>();
-  private final Map<String, Plugin> pluginInstancesByKeys = new HashMap<>();
+public class ServerPluginRepository implements PluginRepository {
+  private final Map<String, ServerPlugin> pluginByKey = new HashMap<>();
   private final Map<ClassLoader, String> keysByClassLoader = new HashMap<>();
 
-  public ServerPluginRepository(SonarRuntime runtime, ServerFileSystem fs, PluginLoader loader) {
-    this.runtime = runtime;
-    this.fs = fs;
-    this.loader = loader;
-  }
-
-  @VisibleForTesting
-  void setBlacklistedPluginKeys(Set<String> keys) {
-    this.blacklistedPluginKeys = keys;
-  }
-
-  @Override
-  public void start() {
-    loadPreInstalledPlugins();
-    moveDownloadedPlugins();
-    unloadIncompatiblePlugins();
-    logInstalledPlugins();
-    loadInstances();
-    started.set(true);
+  public void addPlugins(List<ServerPlugin> 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<String> 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<String, PluginInfo> 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<PluginInfo> 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<String, Plugin> 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<String> 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<PluginInfo> 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<String> 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<String, PluginInfo> getPluginInfosByKeys() {
-    return pluginInfosByKeys;
-  }
-
-  @Override
-  public Collection<PluginInfo> getPluginInfos() {
-    checkState(started.get(), NOT_STARTED_YET);
-    return ImmutableList.copyOf(pluginInfosByKeys.values());
+  public Collection<ServerPlugin> 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<ServerPlugin> 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<Plugin> getPluginInstances() {
+    return pluginByKey.values().stream()
+      .map(ServerPlugin::getInstance)
+      .collect(Collectors.toList());
   }
 
-  private static Collection<File> 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);
   }
 }
index 4fd62aedbad492ad1a13a93d9acde8256145427d..29e1857bce8076e73123b3b6544c002197b7c22d 100644 (file)
  */
 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<UpdateCenter> 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() {
index 08d0b9d4592fa085a381038d345337ca393ee3e6..210f2b92a78e2a58a9054aa5e940349d96d2a6ed 100644 (file)
@@ -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/PluginCompressorTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginCompressorTest.java
new file mode 100644 (file)
index 0000000..e32b0dd
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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 java.nio.file.Path;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.config.internal.MapSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.plugins.PluginCompressor.PROPERTY_PLUGIN_COMPRESSION_ENABLE;
+
+public class PluginCompressorTest {
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  private MapSettings settings = new MapSettings();
+
+  @Before
+  public void setUp() throws IOException {
+    Path targetFolder = temp.newFolder("target").toPath();
+    Path targetJarPath = targetFolder.resolve("test.jar");
+    Files.createFile(targetJarPath);
+  }
+
+  @Test
+  public void compress_jar_if_compression_enabled() 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, true);
+    PluginCompressor underTest = new PluginCompressor(settings.asConfig());
+
+    PluginFilesAndMd5 installedPlugin = underTest.compress("foo", jar, loadedJar);
+    assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath());
+    assertThat(installedPlugin.getCompressedJar().getFile())
+      .exists()
+      .isFile()
+      .hasName("sonar-foo-plugin.pack.gz")
+      .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");
+    // 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);
+    PluginCompressor underTest = new PluginCompressor(settings.asConfig());
+
+    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())
+      .hasSameTextualContentAs(packedJar);
+  }
+
+  private static File touch(File dir, String filename) throws IOException {
+    File file = new File(dir, filename);
+    FileUtils.write(file, RandomStringUtils.random(10), StandardCharsets.UTF_8);
+    return file;
+  }
+
+  //
+  // @Test
+  // public void should_use_deployed_packed_file() throws IOException {
+  // Path packedPath = sourceFolder.resolve("test.pack.gz");
+  // Files.write(packedPath, new byte[] {1, 2, 3});
+  //
+  // settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, true);
+  // underTest = new PluginFileSystem(settings.asConfig());
+  // underTest.compressJar("key", sourceFolder, targetJarPath);
+  //
+  // assertThat(Files.list(targetFolder)).containsOnly(targetJarPath, targetFolder.resolve("test.pack.gz"));
+  // assertThat(underTest.getPlugins()).hasSize(1);
+  // assertThat(underTest.getPlugins().get("key").getFilename()).isEqualTo("test.pack.gz");
+  //
+  // // check that the file was copied, not generated
+  // assertThat(targetFolder.resolve("test.pack.gz")).hasSameContentAs(packedPath);
+  // }
+
+}
index a8319bcff55149c98b77bdbebe34008ff7b0e633..57dee7d4e35dbfa6c00a58735b5d7b12df0f929b 100644 (file)
@@ -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/PluginFileSystemTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java
deleted file mode 100644 (file)
index 98735e4..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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.file.Files;
-import java.nio.file.Path;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang.RandomStringUtils;
-import org.junit.Before;
-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;
-
-public class PluginFileSystemTest {
-  @Rule
-  public TemporaryFolder temp = new TemporaryFolder();
-
-  private MapSettings settings = new MapSettings();
-
-  @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);
-
-    InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get();
-    assertThat(installedPlugin.getPluginInfo()).isSameAs(info);
-    assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath());
-    assertThat(installedPlugin.getCompressedJar().getFile())
-      .exists()
-      .isFile()
-      .hasName("sonar-foo-plugin.pack.gz")
-      .hasParent(loadedJar.getParentFile());
-  }
-
-  @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);
-
-    InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get();
-    assertThat(installedPlugin.getPluginInfo()).isSameAs(info);
-    assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath());
-    assertThat(installedPlugin.getCompressedJar().getFile())
-      .exists()
-      .isFile()
-      .hasName(packedJar.getName())
-      .hasParent(loadedJar.getParentFile())
-      .hasSameContentAs(packedJar);
-  }
-
-  private static File touch(File dir, String filename) throws IOException {
-    File file = new File(dir, filename);
-    FileUtils.write(file, RandomStringUtils.random(10));
-    return file;
-  }
-
-  //
-  // @Test
-  // public void should_use_deployed_packed_file() throws IOException {
-  // Path packedPath = sourceFolder.resolve("test.pack.gz");
-  // Files.write(packedPath, new byte[] {1, 2, 3});
-  //
-  // settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, true);
-  // underTest = new PluginFileSystem(settings.asConfig());
-  // underTest.compressJar("key", sourceFolder, targetJarPath);
-  //
-  // assertThat(Files.list(targetFolder)).containsOnly(targetJarPath, targetFolder.resolve("test.pack.gz"));
-  // assertThat(underTest.getPlugins()).hasSize(1);
-  // assertThat(underTest.getPlugins().get("key").getFilename()).isEqualTo("test.pack.gz");
-  //
-  // // check that the file was copied, not generated
-  // assertThat(targetFolder.resolve("test.pack.gz")).hasSameContentAs(packedPath);
-  // }
-
-}
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 (file)
index 0000000..034a7a4
--- /dev/null
@@ -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 (file)
index 0000000..aec253e
--- /dev/null
@@ -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<String> 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<ServerPluginInfo> 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<ServerPluginInfo> 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<ServerPluginInfo> 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<ServerPluginInfo> 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;
+  }
+}
index 0144ead6eb7968ad95f80b102ff1d0dbd5390ed9..5e92e9087e125d1a7a4eaf08e95a662dc248bc4b 100644 (file)
@@ -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 (file)
index 0000000..0816244
--- /dev/null
@@ -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);
+  }
+}
index 68e60276958de763da1b6822542f10654326b914..78a9473b5cb556fdb901fdf1f4781a9b69721055 100644 (file)
@@ -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 (file)
index 0000000..bd47660
--- /dev/null
@@ -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<String, Plugin> 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);
+  }
+}
index ff8b394c7e9862eb5f801b7ebf4a503c9d174a8b..1b834913f852def68577469beaa8555b20ee93c0 100644 (file)
  */
 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<String, PluginInfo> 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));
   }
 }
index 219653eb7c1d805e37199d5cabe88d79c3279706..648bc614a5ee20a2802f7c568d2c50a0706eeb76 100644 (file)
@@ -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();
   }
 }
index fcffc869ba9c743a611d946d0ecccea90d1af964..5609664ba29fca3bb8244eb3adf8d277d619b1e5 100644 (file)
  */
 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> updateCenter = underTest.getUpdateCenter(false);
 
-    assertThat(updateCenter).isAbsent();
+    assertThat(updateCenter).isEmpty();
   }
 }
index 78609fa838d7fd0b9911a3c1e990f385a56b52de..045ce07e2de24b1ca661acbd3640f71d96bab43c 100644 (file)
@@ -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();
   }
 
 }
index 85710a00b1257de2d4ff9533867385481cbecfce..9055589c4ee44fc4c077cc56768e1583251ec4c0 100644 (file)
@@ -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<String, PluginDto> 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);
+    }
+  }
+
 }
index 73d262e9ff4f68235e6a3d1aef5ba508ea563830..d93d0df6b5dbd9f2d06679d0077b8bca8931cf48 100644 (file)
@@ -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<String> 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);
   }
 }
index 18d4b454f79fe209b23e65cc25d02f4161fffab6..cb13a5200bbda0df2744d636418a5275d7731b6e 100644 (file)
@@ -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<String, PluginDto> 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<String, PluginDto> 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<String, PluginDto> 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);
index 042fe1bbbfe2b426db641fc283581efe42557e73..7dee70266d1e399e6f5052341a2f266b648fa31a 100644 (file)
@@ -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;
index 108210c2f2f857eb799ca0686bfab59ee7e71379..dcfc53c9a563b19e2a31b8717b29343d7e522184 100644 (file)
  */
 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<PluginUpdate> 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());
   }
 
 }
index c837f38ac078c7c0ce583d14aebf0a8011fef883..fdccdd69131311379ddf218196109a47bfeff4e2 100644 (file)
@@ -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<InstalledPlugin> file = pluginFileSystem.getInstalledPlugin(pluginKey);
+    Optional<ServerPlugin> 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())) {
index 7ddfef1d761298bd8a05465b20d5326f1950bf38..ee9963cb9f5a887c09eb3a42e8cef4e058adca60 100644 (file)
@@ -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;
index 485794fc62a15adb7b12110078ec66009ce81c7a..519b5b6872fa83bbdf4934f19b10df0a5c07c75c 100644 (file)
 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 {
         "<li>%s - category as defined in the Update Center. A connection to the Update Center is needed</li>" +
         "</lu>", 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<InstalledPlugin> installedPlugins = loadInstalledPlugins();
+    String typeParam = request.param(PARAM_TYPE);
+    SortedSet<ServerPlugin> installedPlugins = loadInstalledPlugins(typeParam);
     Map<String, PluginDto> 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<InstalledPlugin> loadInstalledPlugins() {
-    return copyOf(NAME_KEY_COMPARATOR, pluginFileSystem.getInstalledFiles());
+  private SortedSet<ServerPlugin> 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());
   }
 }
index 5f1726e56f486a613c4e4cefbe1283a835617568..220f142ea8d7cf7673fece4ab307203107a7ecd5 100644 (file)
  */
 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<String, Plugin> compatiblePluginsByKey) {
     Collection<PluginInfo> uninstalledPlugins = pluginUninstaller.getUninstalledPlugins();
     Collection<PluginInfo> downloadedPlugins = pluginDownloader.getDownloadedPlugins();
-    Collection<PluginInfo> installedPlugins = installer.getPluginInfos();
-    MatchPluginKeys matchPluginKeys = new MatchPluginKeys(from(installedPlugins).transform(PluginInfoToKey.INSTANCE).toSet());
+    Collection<PluginInfo> installedPlugins = serverPluginRepository.getPluginInfos();
+    Set<String> installedPluginKeys = installedPlugins.stream().map(PluginInfo::getKey).collect(Collectors.toSet());
 
     Collection<PluginInfo> newPlugins = new ArrayList<>();
     Collection<PluginInfo> 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<PluginInfo, String> {
-    INSTANCE;
-
-    @Override
-    public String apply(@Nonnull PluginInfo input) {
-      return input.getKey();
-    }
-  }
-
-  private static class MatchPluginKeys implements Predicate<PluginInfo> {
-    private final Set<String> pluginKeys;
-
-    private MatchPluginKeys(Collection<String> pluginKeys) {
-      this.pluginKeys = copyOf(pluginKeys);
-    }
-
-    @Override
-    public boolean apply(@Nonnull PluginInfo input) {
-      return pluginKeys.contains(input.getKey());
-    }
-  }
 }
index 9704a8c89b043928be3f31b372d0b7855d6051a5..9caea752d46abfaa127a7b98ac095dd481a37558 100644 (file)
 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<PluginInfo> NAME_KEY_PLUGIN_METADATA_COMPARATOR = Ordering.natural()
     .onResultOf(PluginInfo::getName)
     .compound(Ordering.natural().onResultOf(PluginInfo::getKey));
-  public static final Comparator<InstalledPlugin> NAME_KEY_COMPARATOR = Comparator
-    .comparing((java.util.function.Function<InstalledPlugin, String>) installedPluginFile -> installedPluginFile.getPluginInfo().getName())
+  public static final Comparator<ServerPlugin> NAME_KEY_COMPARATOR = Comparator
+    .comparing((java.util.function.Function<ServerPlugin, String>) installedPluginFile -> installedPluginFile.getPluginInfo().getName())
     .thenComparing(f -> f.getPluginInfo().getKey());
   public static final Comparator<Plugin> 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();
   }
index 6fc841762ee6cd9032c701d7a4de011fce8b0805..5193350928529857d0d2c3471c287e49a603eb9e 100644 (file)
@@ -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();
   }
-
 }
index 69c802e9a562d7305c55360023ace9e4815a5d67..d142e15854b71c28d8750a90ae5fcc392864c1b4 100644 (file)
  */
 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<PluginUpdate> {
-    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());
-    }
-  }
 }
index b6201298727806a12f8945b58c477a35be1a433e..aef313273c1e4f03856597a7118615bfeb80fa32 100644 (file)
  */
 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;
index 3f1af6853cb498baf349525bb064f667669844e3..4dd4d6ce7b9f236a7415b2f654f265390cd61d15 100644 (file)
@@ -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");
 
index 1ba9387ddf29019c36ed0d27d22697b124ebecb5..fecc923f3e41d6d0c8e898f751f039ef67e79716 100644 (file)
@@ -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();
 
index a893e478e615be4ae41b1ec899d568bb032a2a3b..60d60e683e30b1ae418b373ae2c0074db95c97a4 100644 (file)
@@ -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;
index 726d7ce490ba4a34c87a4a1c4225f306eb616a9d..5939880a23c9972a91f7e3c65a1ebd08e64c3ea8 100644 (file)
@@ -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();
 
index 3cc798a12be58ab2aa3e8edd3e1820207e5a3ac0..1a579ae1192fc7312cfe683e169ce5a11b49215c 100644 (file)
@@ -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 {
index 172c52ffa74399112c09cfa598d764f4aa27c597..8c73582d38ab128703958692c4fdb0c8ea8e4c34 100644 (file)
  */
 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'");
index dae4f95aebc78a3f73cbd6186cbc5b13bf2a6d75..11e833ff1d9eb0328abe15ccf371fbf23234a011 100644 (file)
  */
 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);
   }
 
 }
index 91778eb05edc24d53f81b5fca5ef5c029142032e..a5653c14d0a41dbcb5f4af5e96d0fee29f33eb4a 100644 (file)
@@ -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();
 
index 1c01b48b1a1a1b37d72a189d78b59b1df03c2e51..d0a43431ed932131d5c4963fbbab86269c175dd3 100644 (file)
@@ -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");
index a11b034c5942fbf3e7039f4868f4b60d52228243..8d4145fea2d83f47b57d30253bf8ca5250f3a436 100644 (file)
  */
 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)
index f3501176c1b57d67c38014abe9b12b7915c975df..a3954f67dd6c11ed11380a5daaef3c3f52305b4a 100644 (file)
@@ -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/InstalledPluginsActionTest.java
deleted file mode 100644 (file)
index e4e22e2..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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.updatecenter.ws;
-
-import org.junit.Test;
-import org.sonar.api.server.ws.WebService;
-import org.sonar.core.platform.PluginInfo;
-import org.sonar.server.plugins.ServerPluginRepository;
-import org.sonar.server.ws.WsActionTester;
-import org.sonar.updatecenter.common.Version;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.test.JsonAssert.assertJson;
-
-public class InstalledPluginsActionTest {
-
-  private ServerPluginRepository pluginRepository = mock(ServerPluginRepository.class);
-
-  private WsActionTester ws = new WsActionTester(new InstalledPluginsAction(pluginRepository));
-
-  @Test
-  public void return_plugins() {
-    when(pluginRepository.getPluginInfos()).thenReturn(asList(
-      new PluginInfo("java").setName("Java").setVersion(Version.create("3.14")),
-      new PluginInfo("xoo").setName("Xoo").setVersion(Version.create("1.0"))));
-
-    String result = ws.newRequest().execute().getInput();
-
-    assertJson(result).isSimilarTo("[" +
-      "  {" +
-      "    \"key\": \"java\",\n" +
-      "    \"name\": \"Java\",\n" +
-      "    \"version\": \"3.14\"\n" +
-      "  }," +
-      "  {" +
-      "    \"key\": \"xoo\",\n" +
-      "    \"name\": \"Xoo\",\n" +
-      "    \"version\": \"1.0\"\n" +
-      "  }" +
-      "]");
-  }
-
-  @Test
-  public void return_plugins_with_plugin_having_no_version() {
-    when(pluginRepository.getPluginInfos()).thenReturn(singletonList(
-      new PluginInfo("java").setName("Java")));
-
-    String result = ws.newRequest().execute().getInput();
-
-    assertJson(result).isSimilarTo("[" +
-      "  {" +
-      "    \"key\": \"java\",\n" +
-      "    \"name\": \"Java\"\n" +
-      "  }" +
-      "]");
-  }
-
-  @Test
-  public void test_example() {
-    when(pluginRepository.getPluginInfos()).thenReturn(asList(
-      new PluginInfo("findbugs").setName("Findbugs").setVersion(Version.create("2.1")),
-      new PluginInfo("l10nfr").setName("French Pack").setVersion(Version.create("1.10")),
-      new PluginInfo("jira").setName("JIRA").setVersion(Version.create("1.2"))));
-
-    String result = ws.newRequest().execute().getInput();
-
-    assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString());
-  }
-
-  @Test
-  public void test_definition() {
-    WebService.Action action = ws.getDef();
-    assertThat(action).isNotNull();
-    assertThat(action.isInternal()).isTrue();
-    assertThat(action.isPost()).isFalse();
-    assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).hasSize(1);
-  }
-
-}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/PluginsActionTestFilesAndMD5.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/updatecenter/ws/PluginsActionTestFilesAndMD5.java
new file mode 100644 (file)
index 0000000..1f78451
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * 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.updatecenter.ws;
+
+import org.junit.Test;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.server.plugins.ServerPluginRepository;
+import org.sonar.server.ws.WsActionTester;
+import org.sonar.updatecenter.common.Version;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.test.JsonAssert.assertJson;
+
+public class PluginsActionTestFilesAndMD5 {
+
+  private ServerPluginRepository pluginRepository = mock(ServerPluginRepository.class);
+
+  private WsActionTester ws = new WsActionTester(new InstalledPluginsAction(pluginRepository));
+
+  @Test
+  public void return_plugins() {
+    when(pluginRepository.getPluginInfos()).thenReturn(asList(
+      new PluginInfo("java").setName("Java").setVersion(Version.create("3.14")),
+      new PluginInfo("xoo").setName("Xoo").setVersion(Version.create("1.0"))));
+
+    String result = ws.newRequest().execute().getInput();
+
+    assertJson(result).isSimilarTo("[" +
+      "  {" +
+      "    \"key\": \"java\",\n" +
+      "    \"name\": \"Java\",\n" +
+      "    \"version\": \"3.14\"\n" +
+      "  }," +
+      "  {" +
+      "    \"key\": \"xoo\",\n" +
+      "    \"name\": \"Xoo\",\n" +
+      "    \"version\": \"1.0\"\n" +
+      "  }" +
+      "]");
+  }
+
+  @Test
+  public void return_plugins_with_plugin_having_no_version() {
+    when(pluginRepository.getPluginInfos()).thenReturn(singletonList(
+      new PluginInfo("java").setName("Java")));
+
+    String result = ws.newRequest().execute().getInput();
+
+    assertJson(result).isSimilarTo("[" +
+      "  {" +
+      "    \"key\": \"java\",\n" +
+      "    \"name\": \"Java\"\n" +
+      "  }" +
+      "]");
+  }
+
+  @Test
+  public void test_example() {
+    when(pluginRepository.getPluginInfos()).thenReturn(asList(
+      new PluginInfo("findbugs").setName("Findbugs").setVersion(Version.create("2.1")),
+      new PluginInfo("l10nfr").setName("French Pack").setVersion(Version.create("1.10")),
+      new PluginInfo("jira").setName("JIRA").setVersion(Version.create("1.2"))));
+
+    String result = ws.newRequest().execute().getInput();
+
+    assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString());
+  }
+
+  @Test
+  public void test_definition() {
+    WebService.Action action = ws.getDef();
+    assertThat(action).isNotNull();
+    assertThat(action.isInternal()).isTrue();
+    assertThat(action.isPost()).isFalse();
+    assertThat(action.responseExampleAsString()).isNotEmpty();
+    assertThat(action.params()).hasSize(1);
+  }
+
+}
index cfbb6f6d802468bb8aca0cb1141b1e775742c5f9..40361b6e4b10ec1705a552231778d934e62c3049 100644 (file)
@@ -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,
index 79cf44d96c8a68534b842eb26b0331f0e0449007..296a3d6f872673513e876ff3bb45d85cdd17b79d 100644 (file)
@@ -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(),
index 09d846aec0458028471cb72928664f8b9772044c..bda09ba02c133a5e96c4a90dfc053384034dd6e1 100644 (file)
@@ -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<File> libs;
 
-  public ExplodedPlugin(String key, File main, Collection<File> libs) {
+  public ExplodedPlugin(PluginInfo pluginInfo, String key, File main, Collection<File> libs) {
+    this.pluginInfo = pluginInfo;
     this.key = key;
     this.main = main;
     this.libs = libs;
@@ -45,4 +46,8 @@ public class ExplodedPlugin {
   public Collection<File> getLibs() {
     return libs;
   }
+
+  public PluginInfo getPluginInfo() {
+    return pluginInfo;
+  }
 }
diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java
new file mode 100644 (file)
index 0000000..503bc4d
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * 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.core.platform;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+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.Collections.singleton;
+
+/**
+ * Loads the plugin JAR files by creating the appropriate classloaders and by instantiating
+ * the entry point classes as defined in manifests. It assumes that JAR files are compatible with current
+ * environment (minimal sonarqube version, compatibility between plugins, ...):
+ * <ul>
+ * <li>server verifies compatibility of JARs before deploying them at startup (see ServerPluginRepository)</li>
+ * <li>batch loads only the plugins deployed on server (see BatchPluginRepository)</li>
+ * </ul>
+ * <p/>
+ * Plugins have their own isolated classloader, inheriting only from API classes.
+ * Some plugins can extend a "base" plugin, sharing the same classloader.
+ * <p/>
+ * This class is stateless. It does not keep pointers to classloaders and {@link org.sonar.api.Plugin}.
+ */
+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 PluginClassloaderFactory classloaderFactory;
+
+  public PluginClassLoader(PluginClassloaderFactory classloaderFactory) {
+    this.classloaderFactory = classloaderFactory;
+  }
+
+  public Map<String, Plugin> load(Collection<ExplodedPlugin> plugins) {
+    return load(plugins.stream().collect(Collectors.toMap(ExplodedPlugin::getKey, x -> x)));
+  }
+
+  public Map<String, Plugin> load(Map<String, ExplodedPlugin> pluginsByKey) {
+    Collection<PluginClassLoaderDef> defs = defineClassloaders(pluginsByKey);
+    Map<PluginClassLoaderDef, ClassLoader> classloaders = classloaderFactory.create(defs);
+    return instantiatePluginClasses(classloaders);
+  }
+
+  /**
+   * Defines the different classloaders to be created. Number of classloaders can be
+   * different than number of plugins.
+   */
+  @VisibleForTesting
+  Collection<PluginClassLoaderDef> defineClassloaders(Map<String, ExplodedPlugin> pluginsByKey) {
+    Map<String, PluginClassLoaderDef> classloadersByBasePlugin = new HashMap<>();
+
+    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);
+      }
+      def.addFiles(singleton(plugin.getMain()));
+      def.addFiles(plugin.getLibs());
+      def.addMainClass(info.getKey(), info.getMainClass());
+
+      for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) {
+        def.getExportMask().addInclusion(String.format("%s/%s/api/", defaultSharedResource, info.getKey()));
+      }
+
+      // The plugins that extend other plugins can only add some files to classloader.
+      // They can't change metadata like ordering strategy or compatibility mode.
+      if (Strings.isNullOrEmpty(info.getBasePlugin())) {
+        if (info.isUseChildFirstClassLoader()) {
+          Loggers.get(getClass()).warn("Plugin {} [{}] uses a child first classloader which is deprecated", info.getName(), info.getKey());
+        }
+        def.setSelfFirstStrategy(info.isUseChildFirstClassLoader());
+        Version minSqVersion = info.getMinimalSqVersion();
+        boolean compatibilityMode = minSqVersion != null && minSqVersion.compareToIgnoreQualifier(COMPATIBILITY_MODE_MAX_VERSION) < 0;
+        if (compatibilityMode) {
+          Loggers.get(getClass()).warn("API compatibility mode is no longer supported. In case of error, plugin {} [{}] should package its dependencies.",
+            info.getName(), info.getKey());
+        }
+      }
+    }
+    return classloadersByBasePlugin.values();
+
+  }
+
+  /**
+   * Instantiates collection of {@link org.sonar.api.Plugin} according to given metadata and classloaders
+   *
+   * @return the instances grouped by plugin key
+   * @throws IllegalStateException if at least one plugin can't be correctly loaded
+   */
+  private static Map<String, Plugin> instantiatePluginClasses(Map<PluginClassLoaderDef, ClassLoader> classloaders) {
+    // instantiate plugins
+    Map<String, Plugin> instancesByPluginKey = new HashMap<>();
+    for (Map.Entry<PluginClassLoaderDef, ClassLoader> entry : classloaders.entrySet()) {
+      PluginClassLoaderDef def = entry.getKey();
+      ClassLoader classLoader = entry.getValue();
+
+      // the same classloader can be used by multiple plugins
+      for (Map.Entry<String, String> mainClassEntry : def.getMainClassesByPluginKey().entrySet()) {
+        String pluginKey = mainClassEntry.getKey();
+        String mainClass = mainClassEntry.getValue();
+        try {
+          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);
+        } catch (Throwable e) {
+          throw new IllegalStateException(String.format("Fail to instantiate class [%s] of plugin [%s]", mainClass, pluginKey), e);
+        }
+      }
+    }
+    return instancesByPluginKey;
+  }
+
+  public void unload(Collection<Plugin> plugins) {
+    for (Plugin plugin : plugins) {
+      ClassLoader classLoader = plugin.getClass().getClassLoader();
+      if (classLoader instanceof Closeable && classLoader != classloaderFactory.baseClassLoader()) {
+        try {
+          ((Closeable) classLoader).close();
+        } catch (Exception e) {
+          Loggers.get(getClass()).error("Fail to close classloader " + classLoader.toString(), e);
+        }
+      }
+    }
+  }
+
+  /**
+   * 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.
+   */
+  private static String basePluginKey(PluginInfo plugin, Map<String, ExplodedPlugin> pluginsByKey) {
+    String base = plugin.getKey();
+    String parentKey = plugin.getBasePlugin();
+    while (!Strings.isNullOrEmpty(parentKey)) {
+      PluginInfo parentPlugin = pluginsByKey.get(parentKey).getPluginInfo();
+      base = parentPlugin.getKey();
+      parentKey = parentPlugin.getBasePlugin();
+    }
+    return base;
+  }
+}
index be62f1e885906fbb129a8252dfc0fd822efa6797..fe2a6e3145a18572562345ca50d24eb3af7fff59 100644 (file)
@@ -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<PluginInfo> {
 
   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<PluginInfo> {
       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<PluginInfo> {
 
   @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<PluginInfo> {
     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();
+    }
+  }
 }
index 508aa642666b07f92baf433a1283ec4841c20516..7c97461c034c564732d7d913bdd345f1172d6ce1 100644 (file)
@@ -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<ZipEntry> 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<File> 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/PluginLoader.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java
deleted file mode 100644 (file)
index 5b997d9..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.core.platform;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
-import java.io.Closeable;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-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;
-
-/**
- * Loads the plugin JAR files by creating the appropriate classloaders and by instantiating
- * the entry point classes as defined in manifests. It assumes that JAR files are compatible with current
- * environment (minimal sonarqube version, compatibility between plugins, ...):
- * <ul>
- * <li>server verifies compatibility of JARs before deploying them at startup (see ServerPluginRepository)</li>
- * <li>batch loads only the plugins deployed on server (see BatchPluginRepository)</li>
- * </ul>
- * <p/>
- * Plugins have their own isolated classloader, inheriting only from API classes.
- * Some plugins can extend a "base" plugin, sharing the same classloader.
- * <p/>
- * This class is stateless. It does not keep pointers to classloaders and {@link org.sonar.api.Plugin}.
- */
-public class PluginLoader {
-
-  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;
-    this.classloaderFactory = classloaderFactory;
-  }
-
-  public Map<String, Plugin> load(Map<String, PluginInfo> infoByKeys) {
-    Collection<PluginClassLoaderDef> defs = defineClassloaders(infoByKeys);
-    Map<PluginClassLoaderDef, ClassLoader> classloaders = classloaderFactory.create(defs);
-    return instantiatePluginClasses(classloaders);
-  }
-
-  /**
-   * Defines the different classloaders to be created. Number of classloaders can be
-   * different than number of plugins.
-   */
-  @VisibleForTesting
-  Collection<PluginClassLoaderDef> defineClassloaders(Map<String, PluginInfo> infoByKeys) {
-    Map<String, PluginClassLoaderDef> classloadersByBasePlugin = new HashMap<>();
-
-    for (PluginInfo info : infoByKeys.values()) {
-      String baseKey = basePluginKey(info, infoByKeys);
-      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.addMainClass(info.getKey(), info.getMainClass());
-
-      for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) {
-        def.getExportMask().addInclusion(String.format("%s/%s/api/", defaultSharedResource, info.getKey()));
-      }
-
-      // The plugins that extend other plugins can only add some files to classloader.
-      // They can't change metadata like ordering strategy or compatibility mode.
-      if (Strings.isNullOrEmpty(info.getBasePlugin())) {
-        if (info.isUseChildFirstClassLoader()) {
-          Loggers.get(getClass()).warn("Plugin {} [{}] uses a child first classloader which is deprecated", info.getName(), info.getKey());
-        }
-        def.setSelfFirstStrategy(info.isUseChildFirstClassLoader());
-        Version minSqVersion = info.getMinimalSqVersion();
-        boolean compatibilityMode = minSqVersion != null && minSqVersion.compareToIgnoreQualifier(COMPATIBILITY_MODE_MAX_VERSION) < 0;
-        if (compatibilityMode) {
-          Loggers.get(getClass()).warn("API compatibility mode is no longer supported. In case of error, plugin {} [{}] should package its dependencies.",
-            info.getName(), info.getKey());
-        }
-      }
-    }
-    return classloadersByBasePlugin.values();
-
-  }
-
-  /**
-   * Instantiates collection of {@link org.sonar.api.Plugin} according to given metadata and classloaders
-   *
-   * @return the instances grouped by plugin key
-   * @throws IllegalStateException if at least one plugin can't be correctly loaded
-   */
-  @VisibleForTesting
-  Map<String, Plugin> instantiatePluginClasses(Map<PluginClassLoaderDef, ClassLoader> classloaders) {
-    // instantiate plugins
-    Map<String, Plugin> instancesByPluginKey = new HashMap<>();
-    for (Map.Entry<PluginClassLoaderDef, ClassLoader> entry : classloaders.entrySet()) {
-      PluginClassLoaderDef def = entry.getKey();
-      ClassLoader classLoader = entry.getValue();
-
-      // the same classloader can be used by multiple plugins
-      for (Map.Entry<String, String> mainClassEntry : def.getMainClassesByPluginKey().entrySet()) {
-        String pluginKey = mainClassEntry.getKey();
-        String mainClass = mainClassEntry.getValue();
-        try {
-          instancesByPluginKey.put(pluginKey, (Plugin) classLoader.loadClass(mainClass).newInstance());
-        } catch (UnsupportedClassVersionError 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);
-        }
-      }
-    }
-    return instancesByPluginKey;
-  }
-
-  public void unload(Collection<Plugin> plugins) {
-    for (Plugin plugin : plugins) {
-      ClassLoader classLoader = plugin.getClass().getClassLoader();
-      if (classLoader instanceof Closeable && classLoader != classloaderFactory.baseClassLoader()) {
-        try {
-          ((Closeable) classLoader).close();
-        } catch (Exception e) {
-          Loggers.get(getClass()).error("Fail to close classloader " + classLoader.toString(), e);
-        }
-      }
-    }
-  }
-
-  /**
-   * 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<String, PluginInfo> allPluginsPerKey) {
-    String base = plugin.getKey();
-    String parentKey = plugin.getBasePlugin();
-    while (!Strings.isNullOrEmpty(parentKey)) {
-      PluginInfo parentPlugin = allPluginsPerKey.get(parentKey);
-      base = parentPlugin.getKey();
-      parentKey = parentPlugin.getBasePlugin();
-    }
-    return base;
-  }
-}
index c78a88f6d4938c51022d999495841d38d10f5095..b49599f9c12aa592199bf0812b538cf82c5e914d 100644 (file)
@@ -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<Plugin> getPluginInstances();
+
   boolean hasPlugin(String key);
 }
diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginClassLoaderTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginClassLoaderTest.java
new file mode 100644 (file)
index 0000000..446bb25
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * 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.core.platform;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.assertj.core.data.MapEntry;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.updatecenter.common.Version;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static org.mockito.Mockito.mock;
+
+public class PluginClassLoaderTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Rule
+  public LogTester logTester = new LogTester();
+
+  private PluginClassloaderFactory classloaderFactory = mock(PluginClassloaderFactory.class);
+  private PluginClassLoader underTest = new PluginClassLoader(classloaderFactory);
+
+  @Test
+  public void define_classloader() throws Exception {
+    File jarFile = temp.newFile();
+    PluginInfo plugin = new PluginInfo("foo")
+      .setJarFile(jarFile)
+      .setMainClass("org.foo.FooPlugin")
+      .setMinimalSqVersion(Version.create("5.2"));
+
+    ExplodedPlugin explodedPlugin = createExplodedPlugin(plugin);
+    Collection<PluginClassLoaderDef> 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()).containsAll(explodedPlugin.getLibs());
+    assertThat(def.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin"));
+  }
+
+  /**
+   * A plugin (the "base" plugin) can be extended by other plugins. In this case they share the same classloader.
+   */
+  @Test
+  public void test_plugins_sharing_the_same_classloader() throws Exception {
+    File baseJarFile = temp.newFile();
+    File extensionJar1 = temp.newFile();
+    File extensionJar2 = temp.newFile();
+    PluginInfo base = new PluginInfo("foo")
+      .setJarFile(baseJarFile)
+      .setMainClass("org.foo.FooPlugin")
+      .setUseChildFirstClassLoader(false);
+
+    PluginInfo extension1 = new PluginInfo("fooExtension1")
+      .setJarFile(extensionJar1)
+      .setMainClass("org.foo.Extension1Plugin")
+      .setBasePlugin("foo");
+
+    // This extension tries to change the classloader-ordering strategy of base plugin
+    // (see setUseChildFirstClassLoader(true)).
+    // That is not allowed and should be ignored -> strategy is still the one
+    // defined on base plugin (parent-first in this example)
+    PluginInfo extension2 = new PluginInfo("fooExtension2")
+      .setJarFile(extensionJar2)
+      .setMainClass("org.foo.Extension2Plugin")
+      .setBasePlugin("foo")
+      .setUseChildFirstClassLoader(true);
+
+    ExplodedPlugin baseExplodedPlugin = createExplodedPlugin(base);
+    ExplodedPlugin extension1ExplodedPlugin = createExplodedPlugin(extension1);
+    ExplodedPlugin extension2ExplodedPlugin = createExplodedPlugin(extension2);
+    Collection<PluginClassLoaderDef> defs = underTest.defineClassloaders(ImmutableMap.of(
+      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())
+      .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"));
+  }
+
+  @Test
+  public void log_warning_if_plugin_uses_child_first_classloader() throws IOException {
+    File jarFile = temp.newFile();
+    PluginInfo info = new PluginInfo("foo")
+      .setJarFile(jarFile)
+      .setUseChildFirstClassLoader(true)
+      .setMainClass("org.foo.FooPlugin");
+
+    Collection<PluginClassLoaderDef> defs = underTest.defineClassloaders(
+      ImmutableMap.of("foo", createExplodedPlugin(info)));
+    assertThat(defs).extracting(PluginClassLoaderDef::getBasePluginKey).containsExactly("foo");
+
+    List<String> warnings = logTester.logs(LoggerLevel.WARN);
+    assertThat(warnings).contains("Plugin foo [foo] uses a child first classloader which is deprecated");
+  }
+
+  @Test
+  public void log_warning_if_plugin_is_built_with_api_5_2_or_lower() throws Exception {
+    File jarFile = temp.newFile();
+    PluginInfo info = new PluginInfo("foo")
+      .setJarFile(jarFile)
+      .setMainClass("org.foo.FooPlugin")
+      .setMinimalSqVersion(Version.create("4.5.2"));
+
+    Collection<PluginClassLoaderDef> defs = underTest.defineClassloaders(
+      ImmutableMap.of("foo", createExplodedPlugin(info)));
+    assertThat(defs).extracting(PluginClassLoaderDef::getBasePluginKey).containsExactly("foo");
+
+    List<String> 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.");
+  }
+
+  private ExplodedPlugin createExplodedPlugin(PluginInfo plugin) {
+    return new ExplodedPlugin(plugin, plugin.getKey(), new File(plugin.getKey() + ".jar"), Collections
+      .singleton(new File(plugin.getKey() + "-lib.jar")));
+  }
+}
index ebc67bf175fc986a67b77a48ad7453a4416e4dbb..d85d614c8487e72b5054542c1aaf15a75c351855 100644 (file)
@@ -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) {
index 4e103b1d3493a3f793e7b2d64ebe3c6a804bd8cc..59824693472dee3bf08c9ffbc78e2fd3f3f67af3 100644 (file)
@@ -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-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java
deleted file mode 100644 (file)
index 1d12a4e..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * 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.core.platform;
-
-import com.google.common.collect.ImmutableMap;
-import java.io.File;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import org.assertj.core.data.MapEntry;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.api.utils.log.LogTester;
-import org.sonar.api.utils.log.LoggerLevel;
-import org.sonar.updatecenter.common.Version;
-
-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 {
-
-  @Rule
-  public TemporaryFolder temp = new TemporaryFolder();
-
-  @Rule
-  public LogTester logTester = new LogTester();
-
-  private PluginClassloaderFactory classloaderFactory = mock(PluginClassloaderFactory.class);
-  private PluginLoader underTest = new PluginLoader(new FakePluginExploder(), classloaderFactory);
-
-  @Test
-  public void define_classloader() throws Exception {
-    File jarFile = temp.newFile();
-    PluginInfo info = new PluginInfo("foo")
-      .setJarFile(jarFile)
-      .setMainClass("org.foo.FooPlugin")
-      .setMinimalSqVersion(Version.create("5.2"));
-
-    Collection<PluginClassLoaderDef> defs = underTest.defineClassloaders(ImmutableMap.of("foo", info));
-
-    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.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin"));
-    // TODO test mask - require change in sonar-classloader
-  }
-
-  /**
-   * A plugin (the "base" plugin) can be extended by other plugins. In this case they share the same classloader.
-   */
-  @Test
-  public void test_plugins_sharing_the_same_classloader() throws Exception {
-    File baseJarFile = temp.newFile();
-    File extensionJar1 = temp.newFile();
-    File extensionJar2 = temp.newFile();
-    PluginInfo base = new PluginInfo("foo")
-      .setJarFile(baseJarFile)
-      .setMainClass("org.foo.FooPlugin")
-      .setUseChildFirstClassLoader(false);
-
-    PluginInfo extension1 = new PluginInfo("fooExtension1")
-      .setJarFile(extensionJar1)
-      .setMainClass("org.foo.Extension1Plugin")
-      .setBasePlugin("foo");
-
-    // This extension tries to change the classloader-ordering strategy of base plugin
-    // (see setUseChildFirstClassLoader(true)).
-    // That is not allowed and should be ignored -> strategy is still the one
-    // defined on base plugin (parent-first in this example)
-    PluginInfo extension2 = new PluginInfo("fooExtension2")
-      .setJarFile(extensionJar2)
-      .setMainClass("org.foo.Extension2Plugin")
-      .setBasePlugin("foo")
-      .setUseChildFirstClassLoader(true);
-
-    Collection<PluginClassLoaderDef> defs = underTest.defineClassloaders(ImmutableMap.of(
-      base.getKey(), base, extension1.getKey(), extension1, extension2.getKey(), extension2));
-
-    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.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
-  public void log_warning_if_plugin_uses_child_first_classloader() throws IOException {
-    File jarFile = temp.newFile();
-    PluginInfo info = new PluginInfo("foo")
-      .setJarFile(jarFile)
-      .setUseChildFirstClassLoader(true)
-      .setMainClass("org.foo.FooPlugin");
-
-    Collection<PluginClassLoaderDef> defs = underTest.defineClassloaders(ImmutableMap.of("foo", info));
-    assertThat(defs).extracting(PluginClassLoaderDef::getBasePluginKey).containsExactly("foo");
-
-    List<String> warnings = logTester.logs(LoggerLevel.WARN);
-    assertThat(warnings).contains("Plugin foo [foo] uses a child first classloader which is deprecated");
-  }
-
-  @Test
-  public void log_warning_if_plugin_is_built_with_api_5_2_or_lower() throws Exception {
-    File jarFile = temp.newFile();
-    PluginInfo info = new PluginInfo("foo")
-      .setJarFile(jarFile)
-      .setMainClass("org.foo.FooPlugin")
-      .setMinimalSqVersion(Version.create("4.5.2"));
-
-    Collection<PluginClassLoaderDef> defs = underTest.defineClassloaders(ImmutableMap.of("foo", info));
-    assertThat(defs).extracting(PluginClassLoaderDef::getBasePluginKey).containsExactly("foo");
-
-    List<String> 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());
-    }
-  }
-}
index 6a7f5c663ff0592924cc68370377be7dbcc1df66..9dfd4d3367d9e85d3dd9ff2edc76212fb756388a 100644 (file)
@@ -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,
index c99ab74217a22e23e8d3305494ea28268ac5a78f..107d5311268f21aff7a00d27750af382293dd9dd 100644 (file)
@@ -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);
     }
index 801e3b93ad04d7471b3bb923ee29c0c6f965db48..2d82aee77a028324a95b5742b038bbba95c94061 100644 (file)
@@ -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<String, Plugin> pluginInstancesByKeys;
   private Map<String, ScannerPlugin> pluginsByKeys;
   private Map<ClassLoader, String> 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<String, ExplodedPlugin> 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<Plugin> getPluginInstances() {
+    return pluginInstancesByKeys.values();
+  }
+
   @Override
   public boolean hasPlugin(String key) {
     return pluginsByKeys.containsKey(key);
index e316868c831b06541db62ed24c6c299b4a002d62..268a8403d7ab1e5f2c8b08def67e2359904ab2dc 100644 (file)
 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")));
+    }
+
+  }
 }