From ded90fa8efaec6c497b066500e1d0f4dc531e4d5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Fri, 25 May 2018 11:23:29 +0200 Subject: [PATCH] SONAR-10690 add Core Extension support in SonarQube --- .../container/ComputeEngineContainerImpl.java | 14 +- .../platform/CECoreExtensionsInstaller.java | 32 ++ .../ComputeEngineContainerImplTest.java | 4 +- .../CECoreExtensionsInstallerTest.java | 109 +++++ server/sonar-server-common/build.gradle | 34 ++ .../org/sonar/server/l18n/ServerI18n.java | 58 +++ .../org/sonar/server/l18n/ServerI18nTest.java | 93 ++++ .../org/sonar/l10n/checkstyle.properties | 1 + .../org/sonar/l10n/checkstyle_fr.properties | 1 + .../org/sonar/l10n/core_fr.properties | 2 + .../org/sonar/l10n/coreext.properties | 1 + .../org/sonar/l10n/coreext_fr.properties | 1 + server/sonar-server/build.gradle | 1 + .../platform/WebCoreExtensionsInstaller.java | 32 ++ .../platformlevel/PlatformLevel2.java | 9 +- .../platformlevel/PlatformLevel4.java | 9 +- .../plugins/ServerExtensionInstaller.java | 9 +- .../plugins/StaticResourcesServlet.java | 35 +- .../org/sonar/server/ui/PageRepository.java | 75 +++- .../server/ui/page/CorePageDefinition.java | 37 ++ .../sonar/server/ui/page/package-info.java | 24 + .../WebCoreExtensionsInstallerTest.java | 109 +++++ .../plugins/StaticResourcesServletTest.java | 94 +++- .../sonar/server/ui/PageRepositoryTest.java | 16 +- .../server/ui/ws/ComponentActionTest.java | 5 +- .../sonar/server/ui/ws/GlobalActionTest.java | 5 +- .../server/ui/ws/OrganizationActionTest.java | 5 +- .../server/ui/ws/SettingsActionTest.java | 5 +- settings.gradle | 1 + .../sonar/core/extension/CoreExtension.java | 48 ++ .../extension/CoreExtensionRepository.java | 57 +++ .../CoreExtensionRepositoryImpl.java | 70 +++ .../extension/CoreExtensionsInstaller.java | 166 +++++++ .../core/extension/CoreExtensionsLoader.java | 78 ++++ .../extension/ExtensionProviderSupport.java | 37 ++ .../sonar/core/extension/package-info.java | 24 + .../java/org/sonar/core/i18n/DefaultI18n.java | 14 +- .../core/platform/ComponentContainer.java | 20 +- .../CoreExtensionRepositoryImplTest.java | 177 ++++++++ .../CoreExtensionsInstallerTest.java | 424 ++++++++++++++++++ .../extension/CoreExtensionsLoaderTest.java | 98 ++++ .../scanner/bootstrap/GlobalContainer.java | 11 + .../ScannerCoreExtensionsInstaller.java | 32 ++ .../sonar/scanner/extension/package-info.java | 23 + .../scanner/scan/ModuleScanContainer.java | 13 +- .../scanner/scan/ProjectScanContainer.java | 25 +- .../org/sonar/scanner/task/TaskContainer.java | 21 +- .../ScannerCoreExtensionsInstallerTest.java | 109 +++++ .../scan/ProjectScanContainerTest.java | 3 +- 49 files changed, 2161 insertions(+), 110 deletions(-) create mode 100644 server/sonar-ce/src/main/java/org/sonar/ce/platform/CECoreExtensionsInstaller.java create mode 100644 server/sonar-ce/src/test/java/org/sonar/ce/platform/CECoreExtensionsInstallerTest.java create mode 100644 server/sonar-server-common/build.gradle create mode 100644 server/sonar-server-common/src/main/java/org/sonar/server/l18n/ServerI18n.java create mode 100644 server/sonar-server-common/src/test/java/org/sonar/server/l18n/ServerI18nTest.java create mode 100644 server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle.properties create mode 100644 server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle_fr.properties create mode 100644 server/sonar-server-common/src/test/resources/org/sonar/l10n/core_fr.properties create mode 100644 server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext.properties create mode 100644 server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext_fr.properties create mode 100644 server/sonar-server/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/ui/page/CorePageDefinition.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/ui/page/package-info.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java create mode 100644 sonar-core/src/main/java/org/sonar/core/extension/CoreExtension.java create mode 100644 sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepository.java create mode 100644 sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepositoryImpl.java create mode 100644 sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsInstaller.java create mode 100644 sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsLoader.java create mode 100644 sonar-core/src/main/java/org/sonar/core/extension/ExtensionProviderSupport.java create mode 100644 sonar-core/src/main/java/org/sonar/core/extension/package-info.java create mode 100644 sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionRepositoryImplTest.java create mode 100644 sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsInstallerTest.java create mode 100644 sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsLoaderTest.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstaller.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/package-info.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstallerTest.java diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index e8ef7188e6a..bfc640055d9 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -52,6 +52,7 @@ import org.sonar.ce.cleaning.CeCleaningModule; import org.sonar.ce.db.ReadOnlyPropertiesDao; import org.sonar.ce.log.CeProcessLogging; import org.sonar.ce.notification.ReportAnalysisFailureNotificationModule; +import org.sonar.ce.platform.CECoreExtensionsInstaller; import org.sonar.ce.platform.ComputeEngineExtensionInstaller; import org.sonar.ce.queue.CeQueueCleaner; import org.sonar.ce.queue.PurgeCeActivities; @@ -61,7 +62,6 @@ import org.sonar.ce.taskprocessor.CeTaskProcessorModule; import org.sonar.ce.user.CeUserSession; import org.sonar.core.component.DefaultResourceTypes; import org.sonar.core.config.CorePropertyDefinitions; -import org.sonar.core.i18n.DefaultI18n; import org.sonar.core.i18n.RuleI18nManager; import org.sonar.core.platform.ComponentContainer; import org.sonar.core.platform.Module; @@ -89,6 +89,9 @@ import org.sonar.server.debt.DebtRulesXMLImporter; import org.sonar.server.es.EsModule; import org.sonar.server.es.ProjectIndexersImpl; import org.sonar.server.event.NewAlerts; +import org.sonar.core.extension.CoreExtensionRepositoryImpl; +import org.sonar.core.extension.CoreExtensionsInstaller; +import org.sonar.core.extension.CoreExtensionsLoader; import org.sonar.server.favorite.FavoriteUpdater; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.index.IssueIndex; @@ -104,6 +107,7 @@ import org.sonar.server.issue.notification.NewIssuesNotificationDispatcher; import org.sonar.server.issue.notification.NewIssuesNotificationFactory; import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; +import org.sonar.server.l18n.ServerI18n; import org.sonar.server.measure.index.ProjectMeasuresIndex; import org.sonar.server.measure.index.ProjectMeasuresIndexer; import org.sonar.server.metric.CoreCustomMetrics; @@ -191,6 +195,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { ComponentContainer level2 = this.level1.createChild(); populateLevel2(level2); configureFromModules(level2); + level2.getComponentByType(CoreExtensionsLoader.class).load(); level2.startComponents(); ComponentContainer level3 = level2.createChild(); @@ -204,6 +209,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { configureFromModules(this.level4); ServerExtensionInstaller extensionInstaller = this.level4.getComponentByType(ServerExtensionInstaller.class); extensionInstaller.installExtensions(this.level4); + CoreExtensionsInstaller coreExtensionsInstaller = this.level4.getComponentByType(CECoreExtensionsInstaller.class); + coreExtensionsInstaller.install(this.level4, t -> true); this.level4.startComponents(); startupTasks(); @@ -305,9 +312,11 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { CePluginRepository.class, InstalledPluginReferentialFactory.class, ComputeEngineExtensionInstaller.class, + CoreExtensionRepositoryImpl.class, + CoreExtensionsLoader.class, // depends on plugins - DefaultI18n.class, // used by RuleI18nManager + ServerI18n.class, // used by RuleI18nManager RuleI18nManager.class, // used by DebtRulesXMLImporter Durations.class // used in Web Services and DebtCalculator ); @@ -424,6 +433,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { ServerLogging.class, // privileged plugins + CECoreExtensionsInstaller.class, PrivilegedPluginsBootstraper.class, PrivilegedPluginsStopper.class, diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/platform/CECoreExtensionsInstaller.java b/server/sonar-ce/src/main/java/org/sonar/ce/platform/CECoreExtensionsInstaller.java new file mode 100644 index 00000000000..607726cf88f --- /dev/null +++ b/server/sonar-ce/src/main/java/org/sonar/ce/platform/CECoreExtensionsInstaller.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.platform; + +import org.sonar.api.SonarRuntime; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.extension.CoreExtensionsInstaller; + +@ComputeEngineSide +public class CECoreExtensionsInstaller extends CoreExtensionsInstaller { + public CECoreExtensionsInstaller(SonarRuntime sonarRuntime, CoreExtensionRepository coreExtensionRepository) { + super(sonarRuntime, coreExtensionRepository, ComputeEngineSide.class); + } +} diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index e9c72239901..b079bae241e 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -94,7 +94,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getComponentAdapters()) .hasSize( CONTAINER_ITSELF - + 83 // level 4 + + 84 // level 4 + 21 // content of QualityGateModule + 6 // content of CeConfigurationModule + 4 // content of CeQueueModule @@ -114,7 +114,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getParent().getParent().getComponentAdapters()).hasSize( CONTAINER_ITSELF + 16 // MigrationConfigurationModule - + 17 // level 2 + + 19 // level 2 ); assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize( COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/platform/CECoreExtensionsInstallerTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/platform/CECoreExtensionsInstallerTest.java new file mode 100644 index 00000000000..df075842c66 --- /dev/null +++ b/server/sonar-ce/src/test/java/org/sonar/ce/platform/CECoreExtensionsInstallerTest.java @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.platform; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.stream.Stream; +import org.junit.Test; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.ScannerSide; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; +import org.sonar.core.extension.CoreExtension; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CECoreExtensionsInstallerTest { + private SonarRuntime sonarRuntime = mock(SonarRuntime.class); + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + + private CECoreExtensionsInstaller underTest = new CECoreExtensionsInstaller(sonarRuntime, coreExtensionRepository); + + @Test + public void install_only_adds_ComputeEngineSide_annotated_extension_to_container() { + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of( + new CoreExtension() { + @Override + public String getName() { + return "foo"; + } + + @Override + public void load(Context context) { + context.addExtensions(CeClass.class, ScannerClass.class, WebServerClass.class, + NoAnnotationClass.class, OtherAnnotationClass.class, MultipleAnnotationClass.class); + } + } + )); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertThat(container.getPicoContainer().getComponentAdapters()) + .hasSize(ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2); + assertThat(container.getComponentByType(CeClass.class)).isNotNull(); + assertThat(container.getComponentByType(MultipleAnnotationClass.class)).isNotNull(); + } + + @ComputeEngineSide + public static final class CeClass { + + } + + @ServerSide + public static final class WebServerClass { + + } + + @ScannerSide + public static final class ScannerClass { + + } + + @ServerSide + @ComputeEngineSide + @ScannerSide + public static final class MultipleAnnotationClass { + + } + + public static final class NoAnnotationClass { + + } + + @DarkSide + public static final class OtherAnnotationClass { + + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface DarkSide { + } +} diff --git a/server/sonar-server-common/build.gradle b/server/sonar-server-common/build.gradle new file mode 100644 index 00000000000..12184b6cdaa --- /dev/null +++ b/server/sonar-server-common/build.gradle @@ -0,0 +1,34 @@ +sonarqube { + properties { + property 'sonar.projectName', "${projectTitle} :: Server :: Common" + } +} + +dependencies { + // please keep the list grouped by configuration and ordered by name + + compile 'com.google.guava:guava' + compile 'org.slf4j:slf4j-api' + + compile project(':sonar-core') + compileOnly project(path: ':sonar-plugin-api') + + compileOnly 'com.google.code.findbugs:jsr305' + + testCompile 'com.google.code.findbugs:jsr305' + testCompile 'com.tngtech.java:junit-dataprovider' + testCompile 'junit:junit' + testCompile 'org.assertj:assertj-core' + testCompile 'org.mockito:mockito-core' +} + +//artifactoryPublish.skip = false + +// Used by core plugins +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/l18n/ServerI18n.java b/server/sonar-server-common/src/main/java/org/sonar/server/l18n/ServerI18n.java new file mode 100644 index 00000000000..0f5c55663d6 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/l18n/ServerI18n.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.l18n; + +import com.google.common.annotations.VisibleForTesting; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.System2; +import org.sonar.core.i18n.DefaultI18n; +import org.sonar.core.platform.PluginRepository; +import org.sonar.core.extension.CoreExtension; +import org.sonar.core.extension.CoreExtensionRepository; + +/** + * Subclass of {@link DefaultI18n} which supports Core Extensions. + */ +@ServerSide +@ComputeEngineSide +public class ServerI18n extends DefaultI18n { + private final CoreExtensionRepository coreExtensionRepository; + + public ServerI18n(PluginRepository pluginRepository, System2 system2, CoreExtensionRepository coreExtensionRepository) { + super(pluginRepository, system2); + this.coreExtensionRepository = coreExtensionRepository; + } + + @Override + protected void initialize() { + super.initialize(); + + coreExtensionRepository.loadedCoreExtensions() + .map(CoreExtension::getName) + .forEach(this::initPlugin); + } + + @VisibleForTesting + @Override + protected void doStart(ClassLoader classloader) { + super.doStart(classloader); + } +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/l18n/ServerI18nTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/l18n/ServerI18nTest.java new file mode 100644 index 00000000000..608b2f3fde7 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/l18n/ServerI18nTest.java @@ -0,0 +1,93 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.l18n; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.utils.internal.TestSystem2; +import org.sonar.core.extension.CoreExtension; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.platform.PluginRepository; + +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; + +public class ServerI18nTest { + + private TestSystem2 system2 = new TestSystem2(); + private ServerI18n underTest; + + @Before + public void before() { + PluginRepository pluginRepository = mock(PluginRepository.class); + List plugins = singletonList(newPlugin("checkstyle")); + when(pluginRepository.getPluginInfos()).thenReturn(plugins); + + CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + Stream coreExtensions = Stream.of(newCoreExtension("coreext"), newCoreExtension("othercorext")); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(coreExtensions); + + underTest = new ServerI18n(pluginRepository, system2, coreExtensionRepository); + underTest.doStart(getClass().getClassLoader()); + } + + @Test + public void get_english_labels() { + assertThat(underTest.message(Locale.ENGLISH, "any", null)).isEqualTo("Any"); + assertThat(underTest.message(Locale.ENGLISH, "coreext.rule1.name", null)).isEqualTo("Rule one"); + } + + @Test + public void get_english_labels_when_default_locale_is_not_english() { + Locale defaultLocale = Locale.getDefault(); + try { + Locale.setDefault(Locale.FRENCH); + assertThat(underTest.message(Locale.ENGLISH, "any", null)).isEqualTo("Any"); + assertThat(underTest.message(Locale.ENGLISH, "coreext.rule1.name", null)).isEqualTo("Rule one"); + } finally { + Locale.setDefault(defaultLocale); + } + } + + @Test + public void get_labels_from_french_pack() { + assertThat(underTest.message(Locale.FRENCH, "coreext.rule1.name", null)).isEqualTo("Rule un"); + assertThat(underTest.message(Locale.FRENCH, "any", null)).isEqualTo("Tous"); + } + + private static PluginInfo newPlugin(String key) { + PluginInfo plugin = mock(PluginInfo.class); + when(plugin.getKey()).thenReturn(key); + return plugin; + } + + private static CoreExtension newCoreExtension(String name) { + CoreExtension res = mock(CoreExtension.class); + when(res.getName()).thenReturn(name); + return res; + } + +} diff --git a/server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle.properties b/server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle.properties new file mode 100644 index 00000000000..10fa9295c44 --- /dev/null +++ b/server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle.properties @@ -0,0 +1 @@ +checkstyle.rule1.name=Rule one diff --git a/server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle_fr.properties b/server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle_fr.properties new file mode 100644 index 00000000000..b2fc8f9651f --- /dev/null +++ b/server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle_fr.properties @@ -0,0 +1 @@ +checkstyle.rule1.name=Rule un \ No newline at end of file diff --git a/server/sonar-server-common/src/test/resources/org/sonar/l10n/core_fr.properties b/server/sonar-server-common/src/test/resources/org/sonar/l10n/core_fr.properties new file mode 100644 index 00000000000..9b473d07f5c --- /dev/null +++ b/server/sonar-server-common/src/test/resources/org/sonar/l10n/core_fr.properties @@ -0,0 +1,2 @@ +any=Tous +empty= diff --git a/server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext.properties b/server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext.properties new file mode 100644 index 00000000000..e84fc7ce875 --- /dev/null +++ b/server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext.properties @@ -0,0 +1 @@ +coreext.rule1.name=Rule one diff --git a/server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext_fr.properties b/server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext_fr.properties new file mode 100644 index 00000000000..0cd4598e2fb --- /dev/null +++ b/server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext_fr.properties @@ -0,0 +1 @@ +coreext.rule1.name=Rule un diff --git a/server/sonar-server/build.gradle b/server/sonar-server/build.gradle index 78bf82df662..6b4fd8a88a6 100644 --- a/server/sonar-server/build.gradle +++ b/server/sonar-server/build.gradle @@ -51,6 +51,7 @@ dependencies { compile project(':server:sonar-db-migration') compile project(':server:sonar-plugin-bridge') compile project(':server:sonar-process') + compile project(':server:sonar-server-common') compile project(':sonar-core') compile project(':sonar-scanner-protocol') compile(project(':sonar-markdown')) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java b/server/sonar-server/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java new file mode 100644 index 00000000000..7ab01bc96f9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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; + +import org.sonar.api.SonarRuntime; +import org.sonar.api.server.ServerSide; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.extension.CoreExtensionsInstaller; + +@ServerSide +public class WebCoreExtensionsInstaller extends CoreExtensionsInstaller { + public WebCoreExtensionsInstaller(SonarRuntime sonarRuntime, CoreExtensionRepository coreExtensionRepository) { + super(sonarRuntime, coreExtensionRepository, ServerSide.class); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java index ca1ef1020e5..b32ad3d14e9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java @@ -20,10 +20,12 @@ package org.sonar.server.platform.platformlevel; import org.sonar.api.utils.Durations; -import org.sonar.core.i18n.DefaultI18n; import org.sonar.core.i18n.RuleI18nManager; import org.sonar.core.platform.PluginClassloaderFactory; import org.sonar.core.platform.PluginLoader; +import org.sonar.core.extension.CoreExtensionRepositoryImpl; +import org.sonar.core.extension.CoreExtensionsLoader; +import org.sonar.server.l18n.ServerI18n; import org.sonar.server.platform.DatabaseServerCompatibility; import org.sonar.server.platform.DefaultServerUpgradeStatus; import org.sonar.server.platform.StartupMetadataProvider; @@ -70,9 +72,11 @@ public class PlatformLevel2 extends PlatformLevel { PluginClassloaderFactory.class, InstalledPluginReferentialFactory.class, WebServerExtensionInstaller.class, + CoreExtensionRepositoryImpl.class, + CoreExtensionsLoader.class, // depends on plugins - DefaultI18n.class, + ServerI18n.class, RuleI18nManager.class); // Migration state must be kept at level2 to survive moving in and then out of safe mode @@ -95,6 +99,7 @@ public class PlatformLevel2 extends PlatformLevel { public PlatformLevel start() { // ensuring the HistoryTable exists must be the first thing done when this level is started getOptional(MigrationHistoryTable.class).ifPresent(MigrationHistoryTable::start); + get(CoreExtensionsLoader.class).load(); return super.start(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index e9988fbc7c9..928da0fd9fa 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -32,6 +32,7 @@ import org.sonar.ce.CeModule; import org.sonar.ce.notification.ReportAnalysisFailureNotificationModule; import org.sonar.ce.settings.ProjectConfigurationFactory; import org.sonar.core.component.DefaultResourceTypes; +import org.sonar.core.platform.ComponentContainer; import org.sonar.core.timemachine.Periods; import org.sonar.server.authentication.AuthenticationModule; import org.sonar.server.authentication.LogOAuthWarning; @@ -66,6 +67,7 @@ import org.sonar.server.es.metadata.EsDbCompatibilityImpl; import org.sonar.server.es.metadata.MetadataIndex; import org.sonar.server.es.metadata.MetadataIndexDefinition; import org.sonar.server.event.NewAlerts; +import org.sonar.core.extension.CoreExtensionsInstaller; import org.sonar.server.favorite.FavoriteModule; import org.sonar.server.health.NodeHealthModule; import org.sonar.server.issue.AddTagsAction; @@ -115,6 +117,7 @@ import org.sonar.server.platform.ClusterVerification; import org.sonar.server.platform.PersistentSettings; import org.sonar.server.platform.ServerLogging; import org.sonar.server.platform.SettingsChangeNotifier; +import org.sonar.server.platform.WebCoreExtensionsInstaller; import org.sonar.server.platform.monitoring.WebSystemInfoModule; import org.sonar.server.platform.web.WebPagesFilter; import org.sonar.server.platform.web.requestid.HttpRequestIdModule; @@ -544,6 +547,7 @@ public class PlatformLevel4 extends PlatformLevel { ProjectBadgesWsModule.class, // privileged plugins + WebCoreExtensionsInstaller.class, PrivilegedPluginsBootstraper.class, PrivilegedPluginsStopper.class, @@ -590,7 +594,10 @@ public class PlatformLevel4 extends PlatformLevel { @Override public PlatformLevel start() { ServerExtensionInstaller extensionInstaller = get(ServerExtensionInstaller.class); - extensionInstaller.installExtensions(getContainer()); + CoreExtensionsInstaller coreExtensionsInstaller = get(WebCoreExtensionsInstaller.class); + ComponentContainer container = getContainer(); + extensionInstaller.installExtensions(container); + coreExtensionsInstaller.install(container, t -> true); super.start(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java index ae94ead30eb..16bcba75ca9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java +++ b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java @@ -36,6 +36,7 @@ import org.sonar.core.platform.PluginInfo; import org.sonar.core.platform.PluginRepository; import static java.util.Objects.requireNonNull; +import static org.sonar.core.extension.ExtensionProviderSupport.isExtensionProvider; /** * Loads the plugins server extensions and injects them to DI container @@ -121,12 +122,4 @@ public abstract class ServerExtensionInstaller { return null; } - static boolean isExtensionProvider(Object extension) { - return isType(extension, ExtensionProvider.class) || extension instanceof ExtensionProvider; - } - - static boolean isType(Object extension, Class extensionClass) { - Class clazz = extension instanceof Class ? (Class) extension : extension.getClass(); - return extensionClass.isAssignableFrom(clazz); - } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java b/server/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java index 3e5ad22e85b..2017eb7c693 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java +++ b/server/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java @@ -33,6 +33,7 @@ import org.apache.commons.lang.StringUtils; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.core.platform.PluginRepository; +import org.sonar.core.extension.CoreExtensionRepository; import org.sonar.server.platform.Platform; import org.sonarqube.ws.MediaTypes; @@ -63,21 +64,25 @@ public class StaticResourcesServlet extends HttpServlet { InputStream in = null; OutputStream out = null; try { + CoreExtensionRepository coreExtensionRepository = system.getCoreExtensionRepository(); PluginRepository pluginRepository = system.getPluginRepository(); - if (!pluginRepository.hasPlugin(pluginKey)) { + + boolean coreExtension = coreExtensionRepository.isInstalled(pluginKey); + if (!coreExtension && !pluginRepository.hasPlugin(pluginKey)) { silentlySendError(response, SC_NOT_FOUND); return; } - in = system.openResourceStream(pluginKey, resource, pluginRepository); - if (in != null) { - // mime type must be set before writing response body - completeContentType(response, resource); - out = response.getOutputStream(); - IOUtils.copy(in, out); - } else { + in = coreExtension ? system.openCoreExtensionResourceStream(resource) : system.openPluginResourceStream(pluginKey, resource, pluginRepository); + if (in == null) { silentlySendError(response, SC_NOT_FOUND); + return; } + + // mime type must be set before writing response body + completeContentType(response, resource); + out = response.getOutputStream(); + IOUtils.copy(in, out); } catch (ClientAbortException e) { LOG.trace("Client canceled loading resource [{}] from plugin [{}]: {}", resource, pluginKey, e); } catch (Exception e) { @@ -128,9 +133,19 @@ public class StaticResourcesServlet extends HttpServlet { return Platform.getInstance().getContainer().getComponentByType(PluginRepository.class); } + CoreExtensionRepository getCoreExtensionRepository() { + return Platform.getInstance().getContainer().getComponentByType(CoreExtensionRepository.class); + } + + @CheckForNull + InputStream openPluginResourceStream(String pluginKey, String resource, PluginRepository pluginRepository) throws Exception { + ClassLoader pluginClassLoader = pluginRepository.getPluginInstance(pluginKey).getClass().getClassLoader(); + return pluginClassLoader.getResourceAsStream(resource); + } + @CheckForNull - InputStream openResourceStream(String pluginKey, String resource, PluginRepository pluginRepository) throws Exception { - return pluginRepository.getPluginInstance(pluginKey).getClass().getClassLoader().getResourceAsStream(resource); + InputStream openCoreExtensionResourceStream(String resource) throws Exception { + return getClass().getClassLoader().getResourceAsStream(resource); } boolean isCommitted(HttpServletResponse response) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java b/server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java index 0929a36d438..f16dc84394a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java +++ b/server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java @@ -20,10 +20,8 @@ package org.sonar.server.ui; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import java.util.Collections; import java.util.List; -import java.util.function.Consumer; +import java.util.stream.Stream; import javax.annotation.Nullable; import org.sonar.api.Startable; import org.sonar.api.server.ServerSide; @@ -33,8 +31,11 @@ import org.sonar.api.web.page.Page.Qualifier; import org.sonar.api.web.page.Page.Scope; import org.sonar.api.web.page.PageDefinition; import org.sonar.core.platform.PluginRepository; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.server.ui.page.CorePageDefinition; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.copyOf; import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; @@ -46,26 +47,66 @@ import static org.sonar.core.util.stream.MoreCollectors.toList; @ServerSide public class PageRepository implements Startable { private final PluginRepository pluginRepository; + private final CoreExtensionRepository coreExtensionRepository; private final List definitions; + private final List corePageDefinitions; private List pages; - public PageRepository(PluginRepository pluginRepository) { + /** + * Used by Pico when there is no {@link PageDefinition}. + */ + public PageRepository(PluginRepository pluginRepository, CoreExtensionRepository coreExtensionRepository) { this.pluginRepository = pluginRepository; + this.coreExtensionRepository = coreExtensionRepository; // in case there's no page definition - this.definitions = Collections.emptyList(); + this.definitions = emptyList(); + this.corePageDefinitions = emptyList(); } - public PageRepository(PluginRepository pluginRepository, PageDefinition[] pageDefinitions) { + /** + * Used by Pico when there is only {@link PageDefinition} provided both by Plugin(s). + */ + public PageRepository(PluginRepository pluginRepository, CoreExtensionRepository coreExtensionRepository, + PageDefinition[] pageDefinitions) { this.pluginRepository = pluginRepository; - this.definitions = ImmutableList.copyOf(pageDefinitions); + this.coreExtensionRepository = coreExtensionRepository; + this.definitions = copyOf(pageDefinitions); + this.corePageDefinitions = emptyList(); + } + + /** + * Used by Pico when there is only {@link PageDefinition} provided both by Core Extension(s). + */ + public PageRepository(PluginRepository pluginRepository, CoreExtensionRepository coreExtensionRepository, + CorePageDefinition[] corePageDefinitions) { + this.pluginRepository = pluginRepository; + this.coreExtensionRepository = coreExtensionRepository; + this.definitions = emptyList(); + this.corePageDefinitions = copyOf(corePageDefinitions); + } + + /** + * Used by Pico when there is {@link PageDefinition} provided both by Core Extension(s) and Plugin(s). + */ + public PageRepository(PluginRepository pluginRepository, CoreExtensionRepository coreExtensionRepository, + PageDefinition[] pageDefinitions, CorePageDefinition[] corePageDefinitions) { + this.pluginRepository = pluginRepository; + this.coreExtensionRepository = coreExtensionRepository; + this.definitions = copyOf(pageDefinitions); + this.corePageDefinitions = copyOf(corePageDefinitions); } @Override public void start() { Context context = new Context(); definitions.forEach(definition -> definition.define(context)); - pages = context.getPages().stream() - .peek(checkPluginExists()) + Context coreContext = new Context(); + corePageDefinitions.stream() + .map(CorePageDefinition::getPageDefinition) + .forEach(definition -> definition.define(coreContext)); + pages = Stream.concat( + context.getPages().stream().peek(this::checkPluginExists), + coreContext.getPages().stream().peek(this::checkCoreExtensionExists)) .sorted(comparing(Page::getKey)) .collect(toList()); } @@ -101,12 +142,16 @@ public class PageRepository implements Startable { return requireNonNull(pages, "Pages haven't been initialized yet"); } - private Consumer checkPluginExists() { - return page -> { - String pluginKey = page.getPluginKey(); - boolean pluginExists = pluginRepository.hasPlugin(pluginKey); - checkState(pluginExists, "Page '%s' references plugin '%s' that does not exist", page.getName(), pluginKey); - }; + private void checkPluginExists(Page page) { + String pluginKey = page.getPluginKey(); + checkState(pluginRepository.hasPlugin(pluginKey), + "Page '%s' references plugin '%s' that does not exist", page.getName(), pluginKey); + } + + private void checkCoreExtensionExists(Page page) { + String coreExtensionName = page.getPluginKey(); + checkState(coreExtensionRepository.isInstalled(coreExtensionName), + "Page '%s' references Core Extension '%s' which is not installed", page.getName(), coreExtensionName); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/page/CorePageDefinition.java b/server/sonar-server/src/main/java/org/sonar/server/ui/page/CorePageDefinition.java new file mode 100644 index 00000000000..595cd0826e6 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ui/page/CorePageDefinition.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ui.page; + +import org.sonar.api.ExtensionPoint; +import org.sonar.api.server.ServerSide; +import org.sonar.api.web.page.PageDefinition; + +/** + * This class must be used by core extensions to declare {@link PageDefinition}(s). + *

+ * This allows to distinguish {@link PageDefinition} provided by plugins from those provided by Core Extensions and + * apply the appropriate security and consistency checks. + */ +@ServerSide +@ExtensionPoint +public interface CorePageDefinition { + + PageDefinition getPageDefinition(); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/page/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/ui/page/package-info.java new file mode 100644 index 00000000000..239574f4451 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ui/page/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.ui.page; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java new file mode 100644 index 00000000000..60eee2cce4d --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.stream.Stream; +import org.junit.Test; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.ScannerSide; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; +import org.sonar.core.extension.CoreExtension; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class WebCoreExtensionsInstallerTest { + private SonarRuntime sonarRuntime = mock(SonarRuntime.class); + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + + private WebCoreExtensionsInstaller underTest = new WebCoreExtensionsInstaller(sonarRuntime, coreExtensionRepository); + + @Test + public void install_only_adds_ServerSide_annotated_extension_to_container() { + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of( + new CoreExtension() { + @Override + public String getName() { + return "foo"; + } + + @Override + public void load(Context context) { + context.addExtensions(CeClass.class, ScannerClass.class, WebServerClass.class, + NoAnnotationClass.class, OtherAnnotationClass.class, MultipleAnnotationClass.class); + } + })); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertThat(container.getPicoContainer().getComponentAdapters()) + .hasSize(ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2); + assertThat(container.getComponentByType(WebServerClass.class)).isNotNull(); + assertThat(container.getComponentByType(MultipleAnnotationClass.class)).isNotNull(); + } + + @ComputeEngineSide + public static final class CeClass { + + } + + @ServerSide + public static final class WebServerClass { + + } + + @ScannerSide + public static final class ScannerClass { + + } + + @ServerSide + @ComputeEngineSide + @ScannerSide + public static final class MultipleAnnotationClass { + + } + + public static final class NoAnnotationClass { + + } + + @DarkSide + public static final class OtherAnnotationClass { + + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface DarkSide { + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/plugins/StaticResourcesServletTest.java b/server/sonar-server/src/test/java/org/sonar/server/plugins/StaticResourcesServletTest.java index fad36be8214..d3b79457c9f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/plugins/StaticResourcesServletTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/plugins/StaticResourcesServletTest.java @@ -40,6 +40,7 @@ import org.sonar.api.utils.log.LogTester; import org.sonar.api.utils.log.LoggerLevel; import org.sonar.core.platform.PluginInfo; import org.sonar.core.platform.PluginRepository; +import org.sonar.core.extension.CoreExtensionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -52,7 +53,8 @@ public class StaticResourcesServletTest { private Server jetty; private PluginRepository pluginRepository = mock(PluginRepository.class); - private TestSystem system = new TestSystem(pluginRepository); + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + private TestSystem system = new TestSystem(pluginRepository, coreExtensionRepository); @Before public void setUp() throws Exception { @@ -82,31 +84,58 @@ public class StaticResourcesServletTest { @Test public void return_content_if_exists_in_installed_plugin() throws Exception { - system.answer = IOUtils.toInputStream("bar"); + system.pluginStream = IOUtils.toInputStream("bar"); when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); Response response = call("/static/myplugin/foo.txt"); assertThat(response.isSuccessful()).isTrue(); assertThat(response.body().string()).isEqualTo("bar"); - assertThat(system.calledResource).isEqualTo("static/foo.txt"); + assertThat(system.pluginResource).isEqualTo("static/foo.txt"); } @Test public void return_content_of_folder_of_installed_plugin() throws Exception { - system.answer = IOUtils.toInputStream("bar"); + system.pluginStream = IOUtils.toInputStream("bar"); when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); Response response = call("/static/myplugin/foo/bar.txt"); assertThat(response.isSuccessful()).isTrue(); assertThat(response.body().string()).isEqualTo("bar"); - assertThat(system.calledResource).isEqualTo("static/foo/bar.txt"); + assertThat(system.pluginResource).isEqualTo("static/foo/bar.txt"); + } + + @Test + public void return_content_of_folder_of_installed_core_extension() throws Exception { + system.coreExtensionStream = IOUtils.toInputStream("bar"); + when(coreExtensionRepository.isInstalled("coreext")).thenReturn(true); + + Response response = call("/static/coreext/foo/bar.txt"); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body().string()).isEqualTo("bar"); + assertThat(system.coreExtensionResource).isEqualTo("static/foo/bar.txt"); + } + + @Test + public void return_content_of_folder_of_installed_core_extension_over_installed_plugin_in_case_of_key_conflict() throws Exception { + system.coreExtensionStream = IOUtils.toInputStream("bar of plugin"); + when(coreExtensionRepository.isInstalled("samekey")).thenReturn(true); + system.coreExtensionStream = IOUtils.toInputStream("bar of core extension"); + when(coreExtensionRepository.isInstalled("samekey")).thenReturn(true); + + Response response = call("/static/samekey/foo/bar.txt"); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body().string()).isEqualTo("bar of core extension"); + assertThat(system.pluginResource).isNull(); + assertThat(system.coreExtensionResource).isEqualTo("static/foo/bar.txt"); } @Test public void mime_type_is_set_on_response() throws Exception { - system.answer = IOUtils.toInputStream("bar"); + system.pluginStream = IOUtils.toInputStream("bar"); when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); Response response = call("/static/myplugin/foo.css"); @@ -117,7 +146,7 @@ public class StaticResourcesServletTest { @Test public void return_404_if_resource_not_found_in_installed_plugin() throws Exception { - system.answer = null; + system.pluginStream = null; when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); Response response = call("/static/myplugin/foo.css"); @@ -129,7 +158,7 @@ public class StaticResourcesServletTest { @Test public void return_404_if_plugin_does_not_exist() throws Exception { - system.answer = null; + system.pluginStream = null; when(pluginRepository.hasPlugin("myplugin")).thenReturn(false); Response response = call("/static/myplugin/foo.css"); @@ -141,7 +170,7 @@ public class StaticResourcesServletTest { @Test public void return_resource_if_exists_in_requested_plugin() throws Exception { - system.answer = IOUtils.toInputStream("bar"); + system.pluginStream = IOUtils.toInputStream("bar"); when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); when(pluginRepository.getPluginInfo("myplugin")).thenReturn(new PluginInfo("myplugin")); @@ -155,7 +184,7 @@ public class StaticResourcesServletTest { @Test public void do_not_fail_nor_log_ERROR_when_response_is_already_committed_and_plugin_does_not_exist() throws Exception { - system.answer = null; + system.pluginStream = null; system.isCommitted = true; when(pluginRepository.hasPlugin("myplugin")).thenReturn(false); @@ -181,7 +210,7 @@ public class StaticResourcesServletTest { @Test public void do_not_fail_nor_log_ERROR_when_response_is_already_committed_and_resource_does_not_exist_in_installed_plugin() throws Exception { system.isCommitted = true; - system.answer = null; + system.pluginStream = null; when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); Response response = call("/static/myplugin/foo.css"); @@ -193,7 +222,7 @@ public class StaticResourcesServletTest { @Test public void do_not_fail_nor_log_not_attempt_to_send_error_if_ClientAbortException_is_raised() throws Exception { - system.answerException = new ClientAbortException("Simulating ClientAbortException"); + system.pluginStreamException = new ClientAbortException("Simulating ClientAbortException"); when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); Response response = call("/static/myplugin/foo.css"); @@ -207,7 +236,7 @@ public class StaticResourcesServletTest { @Test public void do_not_fail_when_response_is_committed_after_other_error() throws Exception { system.isCommitted = true; - system.answerException = new RuntimeException("Simulating a error"); + system.pluginStreamException = new RuntimeException("Simulating a error"); when(pluginRepository.hasPlugin("myplugin")).thenReturn(true); Response response = call("/static/myplugin/foo.css"); @@ -218,16 +247,22 @@ public class StaticResourcesServletTest { private static class TestSystem extends StaticResourcesServlet.System { private final PluginRepository pluginRepository; + private final CoreExtensionRepository coreExtensionRepository; + @Nullable + private InputStream pluginStream; + private Exception pluginStreamException = null; @Nullable - private InputStream answer; - private Exception answerException = null; + private String pluginResource; @Nullable - private String calledResource; + private InputStream coreExtensionStream; + private Exception coreExtensionStreamException = null; + private String coreExtensionResource; private boolean isCommitted = false; private IOException sendErrorException = null; - TestSystem(PluginRepository pluginRepository) { + TestSystem(PluginRepository pluginRepository, CoreExtensionRepository coreExtensionRepository) { this.pluginRepository = pluginRepository; + this.coreExtensionRepository = coreExtensionRepository; } @Override @@ -235,14 +270,29 @@ public class StaticResourcesServletTest { return pluginRepository; } + @Override + CoreExtensionRepository getCoreExtensionRepository() { + return this.coreExtensionRepository; + } + + @CheckForNull + @Override + InputStream openPluginResourceStream(String pluginKey, String resource, PluginRepository pluginRepository) throws Exception { + pluginResource = resource; + if (pluginStreamException != null) { + throw pluginStreamException; + } + return pluginStream; + } + @CheckForNull @Override - InputStream openResourceStream(String pluginKey, String resource, PluginRepository pluginRepository) throws Exception { - calledResource = resource; - if (answerException != null) { - throw answerException; + InputStream openCoreExtensionResourceStream(String resource) throws Exception { + coreExtensionResource = resource; + if (coreExtensionStreamException != null) { + throw coreExtensionStreamException; } - return answer; + return coreExtensionStream; } @Override diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java index 89165d85990..ae5b61f49f8 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java @@ -31,6 +31,7 @@ import org.sonar.api.web.page.Page.Qualifier; import org.sonar.api.web.page.PageDefinition; import org.sonar.core.platform.PluginInfo; import org.sonar.core.platform.PluginRepository; +import org.sonar.core.extension.CoreExtensionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -49,8 +50,9 @@ public class PageRepositoryTest { public LogTester logTester = new LogTester(); private PluginRepository pluginRepository = mock(PluginRepository.class); + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); - private PageRepository underTest = new PageRepository(pluginRepository); + private PageRepository underTest = new PageRepository(pluginRepository, coreExtensionRepository); @Before public void setUp() { @@ -64,7 +66,7 @@ public class PageRepositoryTest { .addPage(Page.builder("my_plugin/K1").setName("N1").build()) .addPage(Page.builder("my_plugin/K3").setName("N3").build()); PageDefinition secondPlugin = context -> context.addPage(Page.builder("my_plugin/K2").setName("N2").build()); - underTest = new PageRepository(pluginRepository, new PageDefinition[]{firstPlugin, secondPlugin}); + underTest = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[]{firstPlugin, secondPlugin}); underTest.start(); List result = underTest.getAllPages(); @@ -87,7 +89,7 @@ public class PageRepositoryTest { .addPage(Page.builder("my_plugin/K4").setName("K4").setScope(GLOBAL).build()) .addPage(Page.builder("my_plugin/K5").setName("K5").setScope(COMPONENT).setComponentQualifiers(Qualifier.VIEW).build()) .addPage(Page.builder("my_plugin/K6").setName("K6").setScope(COMPONENT).setComponentQualifiers(Qualifier.APP).build()); - underTest = new PageRepository(pluginRepository, new PageDefinition[]{plugin}); + underTest = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[]{plugin}); underTest.start(); List result = underTest.getComponentPages(false, Qualifiers.PROJECT); @@ -112,7 +114,7 @@ public class PageRepositoryTest { .addPage(Page.builder("my_plugin/K1").setName("N1").build()) .addPage(Page.builder("my_plugin/K2").setName("N2").build()) .addPage(Page.builder("my_plugin/K3").setName("N3").build()); - underTest = new PageRepository(pluginRepository, new PageDefinition[]{plugin}); + underTest = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[]{plugin}); underTest.start(); List result = underTest.getGlobalPages(false); @@ -131,7 +133,7 @@ public class PageRepositoryTest { .addPage(Page.builder("my_plugin/O2").setName("O2").setScope(ORGANIZATION).build()) .addPage(Page.builder("my_plugin/O3").setName("O3").setScope(ORGANIZATION).build()) .addPage(Page.builder("my_plugin/OA1").setName("OA1").setScope(ORGANIZATION).setAdmin(true).build()); - underTest = new PageRepository(pluginRepository, new PageDefinition[]{plugin}); + underTest = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[]{plugin}); underTest.start(); List result = underTest.getOrganizationPages(false); @@ -146,7 +148,7 @@ public class PageRepositoryTest { PageDefinition plugin = context -> context .addPage(Page.builder("my_plugin/O1").setName("O1").setScope(ORGANIZATION).build()) .addPage(Page.builder("my_plugin/O2").setName("O2").setScope(ORGANIZATION).setAdmin(true).build()); - underTest = new PageRepository(pluginRepository, new PageDefinition[]{plugin}); + underTest = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[]{plugin}); underTest.start(); List result = underTest.getOrganizationPages(true); @@ -170,7 +172,7 @@ public class PageRepositoryTest { PageDefinition plugin42 = context -> context.addPage(Page.builder("plugin_42/my_key").setName("N2").build()); pluginRepository = mock(PluginRepository.class); when(pluginRepository.hasPlugin("governance")).thenReturn(true); - underTest = new PageRepository(pluginRepository, new PageDefinition[]{governance, plugin42}); + underTest = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[]{governance, plugin42}); expectedException.expect(IllegalStateException.class); expectedException.expectMessage("Page 'N2' references plugin 'plugin_42' that does not exist"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java index 7ddcd2b32c0..c214196f65c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java @@ -54,6 +54,7 @@ import org.sonar.db.user.UserDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; +import org.sonar.core.extension.CoreExtensionRepository; import org.sonar.server.organization.BillingValidations; import org.sonar.server.organization.BillingValidationsProxy; import org.sonar.server.qualitygate.QualityGateFinder; @@ -595,7 +596,9 @@ public class ComponentActionTest { PluginRepository pluginRepository = mock(PluginRepository.class); when(pluginRepository.hasPlugin(any())).thenReturn(true); when(pluginRepository.getPluginInfo(any())).thenReturn(new PluginInfo("unused").setVersion(Version.create("1.0"))); - PageRepository pageRepository = new PageRepository(pluginRepository, new PageDefinition[] {context -> { + CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + when(coreExtensionRepository.isInstalled(any())).thenReturn(false); + PageRepository pageRepository = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[] {context -> { for (Page page : pages) { context.addPage(page); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java index 186264842f8..26415593278 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java @@ -34,6 +34,7 @@ import org.sonar.db.DbClient; import org.sonar.db.dialect.H2; import org.sonar.db.dialect.MySql; import org.sonar.server.branch.BranchFeatureRule; +import org.sonar.core.extension.CoreExtensionRepository; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.organization.TestOrganizationFlags; @@ -283,7 +284,9 @@ public class GlobalActionTest { PluginRepository pluginRepository = mock(PluginRepository.class); when(pluginRepository.hasPlugin(any())).thenReturn(true); when(pluginRepository.getPluginInfo(any())).thenReturn(new PluginInfo("unused").setVersion(Version.create("1.0"))); - PageRepository pageRepository = new PageRepository(pluginRepository, new PageDefinition[] {context -> { + CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + when(coreExtensionRepository.isInstalled(any())).thenReturn(false); + PageRepository pageRepository = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[] {context -> { for (Page page : pages) { context.addPage(page); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java index 986475ca37b..e49187b7382 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java @@ -33,6 +33,7 @@ import org.sonar.core.platform.PluginRepository; import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.organization.OrganizationDto; +import org.sonar.core.extension.CoreExtensionRepository; import org.sonar.server.organization.BillingValidations; import org.sonar.server.organization.BillingValidationsProxy; import org.sonar.server.organization.DefaultOrganizationProvider; @@ -248,7 +249,9 @@ public class OrganizationActionTest { PluginRepository pluginRepository = mock(PluginRepository.class); when(pluginRepository.hasPlugin(any())).thenReturn(true); when(pluginRepository.getPluginInfo(any())).thenReturn(new PluginInfo("unused").setVersion(Version.create("1.0"))); - PageRepository pageRepository = new PageRepository(pluginRepository, new PageDefinition[] {context -> { + CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + when(coreExtensionRepository.isInstalled(any())).thenReturn(false); + PageRepository pageRepository = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[] {context -> { for (Page page : pages) { context.addPage(page); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/SettingsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/SettingsActionTest.java index 2b4034c6b24..14085068fbf 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ui/ws/SettingsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ui/ws/SettingsActionTest.java @@ -27,6 +27,7 @@ import org.sonar.api.web.page.PageDefinition; import org.sonar.core.platform.PluginInfo; import org.sonar.core.platform.PluginRepository; import org.sonar.process.ProcessProperties; +import org.sonar.core.extension.CoreExtensionRepository; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ui.PageRepository; import org.sonar.server.ws.WsActionTester; @@ -83,7 +84,9 @@ public class SettingsActionTest { PluginRepository pluginRepository = mock(PluginRepository.class); when(pluginRepository.hasPlugin(any())).thenReturn(true); when(pluginRepository.getPluginInfo(any())).thenReturn(new PluginInfo("unused")); - PageRepository pageRepository = new PageRepository(pluginRepository, new PageDefinition[] {context -> { + CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + when(coreExtensionRepository.isInstalled(any())).thenReturn(false); + PageRepository pageRepository = new PageRepository(pluginRepository, coreExtensionRepository, new PageDefinition[] {context -> { for (Page page : pages) { context.addPage(page); } diff --git a/settings.gradle b/settings.gradle index ff20a6df267..d15f0d6f31c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ include 'server:sonar-plugin-bridge' include 'server:sonar-process' include 'server:sonar-qa-util' include 'server:sonar-server' +include 'server:sonar-server-common' include 'server:sonar-vsts' include 'server:sonar-web' diff --git a/sonar-core/src/main/java/org/sonar/core/extension/CoreExtension.java b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtension.java new file mode 100644 index 00000000000..c8279d81893 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtension.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import java.util.Collection; +import org.sonar.api.SonarRuntime; +import org.sonar.api.config.Configuration; + +public interface CoreExtension { + + /** + * Name of the core extension. + *

+ * Used in the same fashion as the key for a plugin. + */ + String getName(); + + interface Context { + SonarRuntime getRuntime(); + + Configuration getBootConfiguration(); + + Context addExtension(Object component); + + Context addExtensions(Object component, Object... otherComponents); + + Context addExtensions(Collection o); + } + + void load(Context context); +} diff --git a/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepository.java b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepository.java new file mode 100644 index 00000000000..9369bf5ab56 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepository.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import java.util.Set; +import java.util.stream.Stream; + +public interface CoreExtensionRepository { + /** + * Register the loaded Core Extensions in the repository. + * + * @throws IllegalStateException if the setCoreExtensionNames has already been called + */ + void setLoadedCoreExtensions(Set coreExtensions); + + /** + * @return a {@link Stream} of the loaded Core Extensions (if any). + * + * @see CoreExtension#getName() + * @throws IllegalStateException if {@link #setLoadedCoreExtensions(Set)} has not been called yet + */ + Stream loadedCoreExtensions(); + + /** + * Register that the specified Core Extension has been installed. + * + * @throws IllegalArgumentException if the specified {@link CoreExtension} has not been loaded prior to this call + * ({@link #setLoadedCoreExtensions(Set)} + * @throws IllegalStateException if {@link #setLoadedCoreExtensions(Set)} has not been called yet + */ + void installed(CoreExtension coreExtension); + + /** + * Tells whether the repository knows of Core Extension with this exact name. + * + * @see CoreExtension#getName() + * @throws IllegalStateException if {@link #setLoadedCoreExtensions(Set)} has not been called yet + */ + boolean isInstalled(String coreExtensionName); +} diff --git a/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepositoryImpl.java b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepositoryImpl.java new file mode 100644 index 00000000000..5a87e47bbbd --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepositoryImpl.java @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class CoreExtensionRepositoryImpl implements CoreExtensionRepository { + private Set coreExtensions = null; + private Set installedCoreExtensions = null; + + @Override + public void setLoadedCoreExtensions(Set coreExtensions) { + checkState(this.coreExtensions == null, "Repository has already been initialized"); + + this.coreExtensions = ImmutableSet.copyOf(coreExtensions); + this.installedCoreExtensions = new HashSet<>(coreExtensions.size()); + } + + @Override + public Stream loadedCoreExtensions() { + checkInitialized(); + + return coreExtensions.stream(); + } + + @Override + public void installed(CoreExtension coreExtension) { + checkInitialized(); + requireNonNull(coreExtension, "coreExtension can't be null"); + checkArgument(coreExtensions.contains(coreExtension), "Specified CoreExtension has not been loaded first"); + + this.installedCoreExtensions.add(coreExtension); + } + + @Override + public boolean isInstalled(String coreExtensionName) { + checkInitialized(); + + return installedCoreExtensions.stream() + .anyMatch(t -> coreExtensionName.equals(t.getName())); + } + + private void checkInitialized() { + checkState(coreExtensions != null, "Repository has not been initialized yet"); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsInstaller.java b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsInstaller.java new file mode 100644 index 00000000000..a9b175f353e --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsInstaller.java @@ -0,0 +1,166 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import org.sonar.api.ExtensionProvider; +import org.sonar.api.SonarRuntime; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.AnnotationUtils; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.platform.ComponentContainer; + +import static java.util.Objects.requireNonNull; + +public abstract class CoreExtensionsInstaller { + private static final Logger LOG = Loggers.get(CoreExtensionsInstaller.class); + + private final SonarRuntime sonarRuntime; + private final CoreExtensionRepository coreExtensionRepository; + private final Class supportedAnnotationType; + + protected CoreExtensionsInstaller(SonarRuntime sonarRuntime, CoreExtensionRepository coreExtensionRepository, + Class supportedAnnotationType) { + this.sonarRuntime = sonarRuntime; + this.coreExtensionRepository = coreExtensionRepository; + this.supportedAnnotationType = supportedAnnotationType; + } + + public void install(ComponentContainer container, Predicate extensionFilter) { + coreExtensionRepository.loadedCoreExtensions() + .forEach(coreExtension -> install(container, extensionFilter, coreExtension)); + } + + private void install(ComponentContainer container, Predicate extensionFilter, CoreExtension coreExtension) { + String coreExtensionName = coreExtension.getName(); + try { + List providerKeys = addDeclaredExtensions(container, extensionFilter, coreExtension); + addProvidedExtensions(container, extensionFilter, coreExtensionName, providerKeys); + + LOG.info("Installed core extension: " + coreExtensionName); + coreExtensionRepository.installed(coreExtension); + } catch (Exception e) { + throw new RuntimeException("Failed to load core extension " + coreExtensionName, e); + } + } + + private List addDeclaredExtensions(ComponentContainer container, Predicate extensionFilter, + CoreExtension coreExtension) { + ContextImpl context = new ContextImpl(container, extensionFilter, coreExtension.getName()); + coreExtension.load(context); + return context.getProviders(); + } + + private void addProvidedExtensions(ComponentContainer container, Predicate extensionFilter, + String extensionCategory, List providerKeys) { + providerKeys.stream() + .map(providerKey -> (ExtensionProvider) container.getComponentByKey(providerKey)) + .forEach(provider -> addFromProvider(container, extensionFilter, extensionCategory, provider)); + } + + private void addFromProvider(ComponentContainer container, Predicate extensionFilter, + String extensionCategory, ExtensionProvider provider) { + Object obj = provider.provide(); + if (obj != null) { + if (obj instanceof Iterable) { + for (Object ext : (Iterable) obj) { + addSupportedExtension(container, extensionFilter, extensionCategory, ext); + } + } else { + addSupportedExtension(container, extensionFilter, extensionCategory, obj); + } + } + } + + private boolean addSupportedExtension(ComponentContainer container, Predicate extensionFilter, + String extensionCategory, T component) { + if (hasSupportedAnnotation(component) && extensionFilter.test(component)) { + container.addExtension(extensionCategory, component); + return true; + } + return false; + } + + private boolean hasSupportedAnnotation(T component) { + return AnnotationUtils.getAnnotation(component, supportedAnnotationType) != null; + } + + private class ContextImpl implements CoreExtension.Context { + private final ComponentContainer container; + private final Predicate extensionFilter; + private final String extensionCategory; + private final List providers = new ArrayList<>(); + + public ContextImpl(ComponentContainer container, Predicate extensionFilter, String extensionCategory) { + this.container = container; + this.extensionFilter = extensionFilter; + this.extensionCategory = extensionCategory; + } + + @Override + public SonarRuntime getRuntime() { + return sonarRuntime; + } + + @Override + public Configuration getBootConfiguration() { + return Optional.ofNullable(container.getComponentByType(Configuration.class)) + .orElseGet(() -> new MapSettings().asConfig()); + } + + @Override + public CoreExtension.Context addExtension(Object component) { + requireNonNull(component, "component can't be null"); + + if (!addSupportedExtension(container, extensionFilter, extensionCategory, component)) { + container.declareExtension(extensionCategory, component); + } else if (ExtensionProviderSupport.isExtensionProvider(component)) { + providers.add(component); + } + return this; + } + + @Override + public final CoreExtension.Context addExtensions(Object component, Object... otherComponents) { + addExtension(component); + Arrays.stream(otherComponents).forEach(this::addExtension); + return this; + } + + @Override + public CoreExtension.Context addExtensions(Collection components) { + requireNonNull(components, "components can't be null"); + components.forEach(this::addExtension); + return this; + } + + public List getProviders() { + return providers; + } + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsLoader.java b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsLoader.java new file mode 100644 index 00000000000..79d78cb257b --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsLoader.java @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import com.google.common.collect.ImmutableSet; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.util.stream.MoreCollectors; + +import static com.google.common.base.Preconditions.checkState; + +/** + * Load {@link CoreExtension} and register them into the {@link CoreExtensionRepository}. + */ +public class CoreExtensionsLoader { + private static final Logger LOG = Loggers.get(CoreExtensionsLoader.class); + + private final CoreExtensionRepository coreExtensionRepository; + private final ServiceLoaderWrapper serviceLoaderWrapper; + + public CoreExtensionsLoader(CoreExtensionRepository coreExtensionRepository) { + this(coreExtensionRepository, new ServiceLoaderWrapper()); + } + + CoreExtensionsLoader(CoreExtensionRepository coreExtensionRepository, ServiceLoaderWrapper serviceLoaderWrapper) { + this.coreExtensionRepository = coreExtensionRepository; + this.serviceLoaderWrapper = serviceLoaderWrapper; + } + + public void load() { + Set coreExtensions = serviceLoaderWrapper.load(getClass().getClassLoader()); + ensureNoDuplicateName(coreExtensions); + + coreExtensionRepository.setLoadedCoreExtensions(coreExtensions); + LOG.info("Loaded core extensions: {}", coreExtensions.stream().map(CoreExtension::getName).collect(Collectors.joining(", "))); + } + + private static void ensureNoDuplicateName(Set coreExtensions) { + Map nameCounts = coreExtensions.stream() + .map(CoreExtension::getName) + .collect(Collectors.groupingBy(t -> t, Collectors.counting())); + Set duplicatedNames = nameCounts.entrySet().stream() + .filter(t -> t.getValue() > 1) + .map(Map.Entry::getKey) + .collect(MoreCollectors.toSet()); + checkState(duplicatedNames.isEmpty(), + "Multiple core extensions declare the following names: %s", + duplicatedNames.stream().sorted().collect(Collectors.joining(", "))); + } + + static class ServiceLoaderWrapper { + Set load(ClassLoader classLoader) { + ServiceLoader loader = ServiceLoader.load(CoreExtension.class, classLoader); + return ImmutableSet.copyOf(loader.iterator()); + } + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/extension/ExtensionProviderSupport.java b/sonar-core/src/main/java/org/sonar/core/extension/ExtensionProviderSupport.java new file mode 100644 index 00000000000..bc0fae86485 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/extension/ExtensionProviderSupport.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import org.sonar.api.ExtensionProvider; + +public final class ExtensionProviderSupport { + private ExtensionProviderSupport() { + // prevents implementation + } + + public static boolean isExtensionProvider(Object extension) { + return isType(extension, ExtensionProvider.class) || extension instanceof ExtensionProvider; + } + + private static boolean isType(Object extension, Class extensionClass) { + Class clazz = extension instanceof Class ? (Class) extension : extension.getClass(); + return extensionClass.isAssignableFrom(clazz); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/extension/package-info.java b/sonar-core/src/main/java/org/sonar/core/extension/package-info.java new file mode 100644 index 00000000000..49ad26896c3 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/extension/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.core.extension; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java b/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java index fcdff6c4f1a..c181cfbe7c0 100644 --- a/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java +++ b/sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java @@ -43,8 +43,6 @@ import org.apache.commons.io.IOUtils; import org.picocontainer.Startable; import org.sonar.api.batch.ScannerSide; import org.sonar.api.i18n.I18n; -import org.sonar.api.ce.ComputeEngineSide; -import org.sonar.api.server.ServerSide; import org.sonar.api.utils.SonarException; import org.sonar.api.utils.System2; import org.sonar.api.utils.log.Logger; @@ -53,8 +51,6 @@ import org.sonar.core.platform.PluginInfo; import org.sonar.core.platform.PluginRepository; @ScannerSide -@ServerSide -@ComputeEngineSide public class DefaultI18n implements I18n, Startable { private static final Logger LOG = Loggers.get(DefaultI18n.class); @@ -88,10 +84,15 @@ public class DefaultI18n implements I18n, Startable { } @VisibleForTesting - void doStart(ClassLoader classloader) { + protected void doStart(ClassLoader classloader) { this.classloader = classloader; this.propertyToBundles = new HashMap<>(); + initialize(); + LOG.debug("Loaded {} properties from l10n bundles", propertyToBundles.size()); + } + + protected void initialize() { // org.sonar.l10n.core bundle is provided by sonar-core module initPlugin("core"); @@ -99,10 +100,9 @@ public class DefaultI18n implements I18n, Startable { for (PluginInfo plugin : infos) { initPlugin(plugin.getKey()); } - LOG.debug("Loaded {} properties from l10n bundles", propertyToBundles.size()); } - private void initPlugin(String pluginKey) { + protected void initPlugin(String pluginKey) { try { String bundleKey = BUNDLE_PACKAGE + pluginKey; ResourceBundle bundle = ResourceBundle.getBundle(bundleKey, Locale.ENGLISH, this.classloader, control); diff --git a/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java b/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java index 0bcb93396b9..7454777c7a0 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java @@ -43,6 +43,7 @@ import org.sonar.api.server.ServerSide; import static com.google.common.collect.ImmutableList.copyOf; import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; @ScannerSide @ServerSide @@ -228,7 +229,7 @@ public class ComponentContainer implements ContainerPopulator.Container { } catch (Throwable t) { throw new IllegalStateException("Unable to register component " + getName(component), t); } - declareExtension(null, component); + declareExtension("", component); } return this; } @@ -244,6 +245,17 @@ public class ComponentContainer implements ContainerPopulator.Container { return this; } + public ComponentContainer addExtension(@Nullable String defaultCategory, Object extension) { + Object key = componentKeys.of(extension); + try { + pico.as(Characteristics.CACHE).addComponent(key, extension); + } catch (Throwable t) { + throw new IllegalStateException("Unable to register extension " + getName(extension), t); + } + declareExtension(defaultCategory, extension); + return this; + } + private static String getName(Object extension) { if (extension instanceof Class) { return ((Class) extension).getName(); @@ -252,7 +264,11 @@ public class ComponentContainer implements ContainerPopulator.Container { } public void declareExtension(@Nullable PluginInfo pluginInfo, Object extension) { - propertyDefinitions.addComponent(extension, pluginInfo != null ? pluginInfo.getName() : ""); + declareExtension(pluginInfo != null ? pluginInfo.getName() : "", extension); + } + + public void declareExtension(@Nullable String defaultCategory, Object extension) { + propertyDefinitions.addComponent(extension, ofNullable(defaultCategory).orElse("")); } public ComponentContainer addPicoAdapter(ComponentAdapter adapter) { diff --git a/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionRepositoryImplTest.java b/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionRepositoryImplTest.java new file mode 100644 index 00000000000..6764e347257 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionRepositoryImplTest.java @@ -0,0 +1,177 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import com.google.common.collect.ImmutableSet; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(DataProviderRunner.class) +public class CoreExtensionRepositoryImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private CoreExtensionRepositoryImpl underTest = new CoreExtensionRepositoryImpl(); + + @Test + public void loadedCoreExtensions_fails_with_ISE_if_called_before_setLoadedCoreExtensions() { + expectRepositoryNotInitializedISE(); + + underTest.loadedCoreExtensions(); + } + + @Test + @UseDataProvider("coreExtensionsSets") + public void loadedCoreExtensions_returns_CoreExtensions_from_setLoadedCoreExtensions(Set coreExtensions) { + underTest.setLoadedCoreExtensions(coreExtensions); + + assertThat(underTest.loadedCoreExtensions().collect(Collectors.toSet())) + .isEqualTo(coreExtensions); + } + + @Test + public void setLoadedCoreExtensions_fails_with_NPE_if_argument_is_null() { + expectedException.expect(NullPointerException.class); + + underTest.setLoadedCoreExtensions(null); + } + + @Test + @UseDataProvider("coreExtensionsSets") + public void setLoadedCoreExtensions_fails_with_ISE_if_called_twice(Set coreExtensions) { + underTest.setLoadedCoreExtensions(coreExtensions); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Repository has already been initialized"); + + underTest.setLoadedCoreExtensions(coreExtensions); + } + + @Test + public void installed_fails_with_ISE_if_called_before_setLoadedCoreExtensions() { + CoreExtension coreExtension = newCoreExtension(); + + expectRepositoryNotInitializedISE(); + + underTest.installed(coreExtension); + } + + @Test + @UseDataProvider("coreExtensionsSets") + public void installed_fails_with_IAE_if_CoreExtension_is_not_loaded(Set coreExtensions) { + underTest.setLoadedCoreExtensions(coreExtensions); + CoreExtension coreExtension = newCoreExtension(); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Specified CoreExtension has not been loaded first"); + + underTest.installed(coreExtension); + } + + @Test + @UseDataProvider("coreExtensionsSets") + public void installed_fails_with_NPE_if_CoreExtension_is_null(Set coreExtensions) { + underTest.setLoadedCoreExtensions(coreExtensions); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("coreExtension can't be null"); + + underTest.installed(null); + } + + @Test + public void isInstalled_fails_with_ISE_if_called_before_setLoadedCoreExtensions() { + expectRepositoryNotInitializedISE(); + + underTest.isInstalled("foo"); + } + + @Test + @UseDataProvider("coreExtensionsSets") + public void isInstalled_returns_false_for_not_loaded_CoreExtension(Set coreExtensions) { + underTest.setLoadedCoreExtensions(coreExtensions); + + assertThat(underTest.isInstalled("not loaded")).isFalse(); + } + + @Test + public void isInstalled_returns_false_for_loaded_but_not_installed_CoreExtension() { + CoreExtension coreExtension = newCoreExtension(); + underTest.setLoadedCoreExtensions(singleton(coreExtension)); + + assertThat(underTest.isInstalled(coreExtension.getName())).isFalse(); + } + + @Test + public void isInstalled_returns_true_for_loaded_and_installed_CoreExtension() { + CoreExtension coreExtension = newCoreExtension(); + underTest.setLoadedCoreExtensions(singleton(coreExtension)); + underTest.installed(coreExtension); + + assertThat(underTest.isInstalled(coreExtension.getName())).isTrue(); + } + + private void expectRepositoryNotInitializedISE() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Repository has not been initialized yet"); + } + + @DataProvider + public static Object[][] coreExtensionsSets() { + return new Object[][] { + {emptySet()}, + {singleton(newCoreExtension())}, + {ImmutableSet.of(newCoreExtension(), newCoreExtension())}, + }; + } + + private static int nameCounter = 0; + + private static CoreExtension newCoreExtension() { + String name = "name_" + nameCounter; + nameCounter++; + return newCoreExtension(name); + } + + private static CoreExtension newCoreExtension(String name) { + return new CoreExtension() { + @Override + public String getName() { + return name; + } + + @Override + public void load(Context context) { + throw new UnsupportedOperationException("load should not be called"); + } + }; + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsInstallerTest.java b/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsInstallerTest.java new file mode 100644 index 00000000000..d9d03370863 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsInstallerTest.java @@ -0,0 +1,424 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.picocontainer.ComponentAdapter; +import org.sonar.api.ExtensionProvider; +import org.sonar.api.Property; +import org.sonar.api.SonarRuntime; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +@RunWith(DataProviderRunner.class) +public class CoreExtensionsInstallerTest { + private SonarRuntime sonarRuntime = mock(SonarRuntime.class); + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + private CoreExtensionsInstaller underTest = new CoreExtensionsInstaller(sonarRuntime, coreExtensionRepository, WestSide.class) { + + }; + + private ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(CoreExtension.Context.class); + private static int name_counter = 0; + + @Test + public void install_has_no_effect_if_CoreExtensionRepository_has_no_loaded_CoreExtension() { + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertAddedExtensions(container, 0); + } + + @Test + public void install_calls_load_method_on_all_loaded_CoreExtension() { + CoreExtension coreExtension1 = newCoreExtension(); + CoreExtension coreExtension2 = newCoreExtension(); + CoreExtension coreExtension3 = newCoreExtension(); + CoreExtension coreExtension4 = newCoreExtension(); + List coreExtensions = ImmutableList.of(coreExtension1, coreExtension2, coreExtension3, coreExtension4); + InOrder inOrder = Mockito.inOrder(coreExtension1, coreExtension2, coreExtension3, coreExtension4); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(coreExtensions.stream()); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + inOrder.verify(coreExtension1).load(contextCaptor.capture()); + inOrder.verify(coreExtension2).load(contextCaptor.capture()); + inOrder.verify(coreExtension3).load(contextCaptor.capture()); + inOrder.verify(coreExtension4).load(contextCaptor.capture()); + // verify each core extension gets its own Context + assertThat(contextCaptor.getAllValues()) + .hasSameElementsAs(ImmutableSet.copyOf(contextCaptor.getAllValues())); + } + + @Test + public void install_provides_runtime_from_constructor_in_context() { + CoreExtension coreExtension1 = newCoreExtension(); + CoreExtension coreExtension2 = newCoreExtension(); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension1, coreExtension2)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + verify(coreExtension1).load(contextCaptor.capture()); + verify(coreExtension2).load(contextCaptor.capture()); + assertThat(contextCaptor.getAllValues()) + .extracting(CoreExtension.Context::getRuntime) + .containsOnly(sonarRuntime); + } + + @Test + public void install_provides_new_Configuration_when_getBootConfiguration_is_called_and_there_is_none_in_container() { + CoreExtension coreExtension1 = newCoreExtension(); + CoreExtension coreExtension2 = newCoreExtension(); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension1, coreExtension2)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + verify(coreExtension1).load(contextCaptor.capture()); + verify(coreExtension2).load(contextCaptor.capture()); + // verify each core extension gets its own configuration + assertThat(contextCaptor.getAllValues()) + .hasSameElementsAs(ImmutableSet.copyOf(contextCaptor.getAllValues())); + } + + @Test + public void install_provides_Configuration_from_container_when_getBootConfiguration_is_called() { + CoreExtension coreExtension1 = newCoreExtension(); + CoreExtension coreExtension2 = newCoreExtension(); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension1, coreExtension2)); + Configuration configuration = new MapSettings().asConfig(); + ComponentContainer container = new ComponentContainer(); + container.add(configuration); + + underTest.install(container, t -> true); + + verify(coreExtension1).load(contextCaptor.capture()); + verify(coreExtension2).load(contextCaptor.capture()); + assertThat(contextCaptor.getAllValues()) + .extracting(CoreExtension.Context::getBootConfiguration) + .containsOnly(configuration); + } + + @Test + @UseDataProvider("allMethodsToAddExtension") + public void install_installs_extensions_annotated_with_expected_annotation(BiConsumer> extensionAdder) { + List extensions = ImmutableList.of(WestSideClass.class, EastSideClass.class, OtherSideClass.class, Latitude.class); + CoreExtension coreExtension = newCoreExtension(context -> extensionAdder.accept(context, extensions)); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertAddedExtensions(container, WestSideClass.class, Latitude.class); + assertPropertyDefinitions(container); + } + + @Test + @UseDataProvider("allMethodsToAddExtension") + public void install_does_not_install_extensions_annotated_with_expected_annotation_but_filtered_out(BiConsumer> extensionAdder) { + List extensions = ImmutableList.of(WestSideClass.class, EastSideClass.class, OtherSideClass.class, Latitude.class); + CoreExtension coreExtension = newCoreExtension(context -> extensionAdder.accept(context, extensions)); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> t != Latitude.class); + + assertAddedExtensions(container, WestSideClass.class); + assertPropertyDefinitions(container); + } + + @Test + @UseDataProvider("allMethodsToAddExtension") + public void install_adds_PropertyDefinition_from_annotation_no_matter_annotations(BiConsumer> extensionAdder) { + List extensions = ImmutableList.of(WestSidePropertyDefinition.class, EastSidePropertyDefinition.class, + OtherSidePropertyDefinition.class, LatitudePropertyDefinition.class, BlankPropertyDefinition.class); + CoreExtension coreExtension = newCoreExtension(context -> extensionAdder.accept(context, extensions)); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertAddedExtensions(container, WestSidePropertyDefinition.class, LatitudePropertyDefinition.class); + assertPropertyDefinitions(container, "westKey", "eastKey", "otherKey", "latitudeKey", "blankKey"); + } + + @Test + @UseDataProvider("allMethodsToAddExtension") + public void install_adds_PropertyDefinition_from_annotation_no_matter_annotations_even_if_filtered_out(BiConsumer> extensionAdder) { + List extensions = ImmutableList.of(WestSidePropertyDefinition.class, EastSidePropertyDefinition.class, + OtherSidePropertyDefinition.class, LatitudePropertyDefinition.class, BlankPropertyDefinition.class); + CoreExtension coreExtension = newCoreExtension(context -> extensionAdder.accept(context, extensions)); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> false); + + assertAddedExtensions(container, 0); + assertPropertyDefinitions(container, "westKey", "eastKey", "otherKey", "latitudeKey", "blankKey"); + } + + @Test + @UseDataProvider("allMethodsToAddExtension") + public void install_adds_PropertyDefinition_with_extension_name_as_default_category(BiConsumer> extensionAdder) { + PropertyDefinition propertyDefinitionNoCategory = PropertyDefinition.builder("fooKey").build(); + PropertyDefinition propertyDefinitionWithCategory = PropertyDefinition.builder("barKey").category("donut").build(); + List extensions = ImmutableList.of(propertyDefinitionNoCategory, propertyDefinitionWithCategory); + CoreExtension coreExtension = newCoreExtension(context -> extensionAdder.accept(context, extensions)); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertAddedExtensions(container, 0); + assertPropertyDefinitions(container, coreExtension, propertyDefinitionNoCategory, propertyDefinitionWithCategory); + } + + @Test + @UseDataProvider("allMethodsToAddExtension") + public void install_adds_providers_to_container_and_install_extensions_they_provide_when_annotated_with_expected_annotation(BiConsumer> extensionAdder) { + List extensions = ImmutableList.of(WestSideProvider.class, PartiallyWestSideProvider.class, EastSideProvider.class); + CoreExtension coreExtension = newCoreExtension(context -> extensionAdder.accept(context, extensions)); + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(coreExtension)); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertAddedExtensions(container, WestSideProvider.class, WestSideProvided.class, PartiallyWestSideProvider.class); + assertPropertyDefinitions(container); + } + + @DataProvider + public static Object[][] allMethodsToAddExtension() { + BiConsumer> addExtension = (context, objects) -> objects.forEach(context::addExtension); + BiConsumer> addExtensionsVarArg = (context, objects) -> { + if (objects.isEmpty()) { + return; + } + if (objects.size() == 1) { + context.addExtensions(objects.iterator().next()); + } + context.addExtensions(objects.iterator().next(), objects.stream().skip(1).toArray(Object[]::new)); + }; + BiConsumer> addExtensions = CoreExtension.Context::addExtensions; + return new Object[][] { + {addExtension}, + {addExtensions}, + {addExtensionsVarArg} + }; + } + + private static void assertAddedExtensions(ComponentContainer container, int addedExtensions) { + Collection> adapters = container.getPicoContainer().getComponentAdapters(); + assertThat(adapters) + .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + addedExtensions); + } + + private static void assertAddedExtensions(ComponentContainer container, Class... classes) { + Collection> adapters = container.getPicoContainer().getComponentAdapters(); + assertThat(adapters) + .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + classes.length); + + Stream installedExtensions = adapters.stream() + .map(t -> (Class) t.getComponentImplementation()) + .filter(t -> !PropertyDefinitions.class.isAssignableFrom(t) && t != ComponentContainer.class); + assertThat(installedExtensions) + .contains(classes) + .hasSize(classes.length); + } + + private void assertPropertyDefinitions(ComponentContainer container, String... keys) { + PropertyDefinitions propertyDefinitions = container.getComponentByType(PropertyDefinitions.class); + if (keys.length == 0) { + assertThat(propertyDefinitions.getAll()).isEmpty(); + } else { + for (String key : keys) { + assertThat(propertyDefinitions.get(key)).isNotNull(); + } + } + } + + private void assertPropertyDefinitions(ComponentContainer container, CoreExtension coreExtension, PropertyDefinition... definitions) { + PropertyDefinitions propertyDefinitions = container.getComponentByType(PropertyDefinitions.class); + if (definitions.length == 0) { + assertThat(propertyDefinitions.getAll()).isEmpty(); + } else { + for (PropertyDefinition definition : definitions) { + PropertyDefinition actual = propertyDefinitions.get(definition.key()); + assertThat(actual.category()).isEqualTo(definition.category() == null ? coreExtension.getName() : definition.category()); + } + } + } + + private static CoreExtension newCoreExtension() { + return newCoreExtension(t -> { + }); + } + + private static CoreExtension newCoreExtension(Consumer loadImplementation) { + CoreExtension res = mock(CoreExtension.class); + when(res.getName()).thenReturn("name_" + name_counter); + name_counter++; + doAnswer(invocation -> { + CoreExtension.Context context = invocation.getArgument(0); + loadImplementation.accept(context); + return null; + }).when(res).load(any()); + return res; + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface WestSide { + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface EastSide { + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface OtherSide { + } + + @WestSide + public static class WestSideClass { + + } + + @EastSide + public static class EastSideClass { + + } + + @OtherSide + public static class OtherSideClass { + + } + + @WestSide + @EastSide + public static class Latitude { + + } + + @WestSide + public static class WestSideProvider extends ExtensionProvider { + + @Override + public Object provide() { + return WestSideProvided.class; + } + } + + @WestSide + public static class WestSideProvided { + + } + + @WestSide + public static class PartiallyWestSideProvider extends ExtensionProvider { + + @Override + public Object provide() { + return NotWestSideProvided.class; + } + } + + public static class NotWestSideProvided { + + } + + @EastSide + public static class EastSideProvider extends ExtensionProvider { + + @Override + public Object provide() { + throw new IllegalStateException("EastSideProvider#provide should not be called"); + } + } + + @Property(key = "westKey", name = "westName") + @WestSide + public static class WestSidePropertyDefinition { + + } + + @Property(key = "eastKey", name = "eastName") + @EastSide + public static class EastSidePropertyDefinition { + + } + + @Property(key = "otherKey", name = "otherName") + @OtherSide + public static class OtherSidePropertyDefinition { + + } + + @Property(key = "latitudeKey", name = "latitudeName") + @WestSide + @EastSide + public static class LatitudePropertyDefinition { + + } + + @Property(key = "blankKey", name = "blankName") + public static class BlankPropertyDefinition { + + } + +} diff --git a/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsLoaderTest.java b/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsLoaderTest.java new file mode 100644 index 00000000000..180592eff69 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsLoaderTest.java @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.extension; + +import com.google.common.collect.ImmutableSet; +import java.util.Collections; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class CoreExtensionsLoaderTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + private CoreExtensionsLoader.ServiceLoaderWrapper serviceLoaderWrapper = mock(CoreExtensionsLoader.ServiceLoaderWrapper.class); + private CoreExtensionsLoader underTest = new CoreExtensionsLoader(coreExtensionRepository, serviceLoaderWrapper); + + @Test + public void load_has_no_effect_if_there_is_no_ServiceLoader_for_CoreExtension_class() { + when(serviceLoaderWrapper.load(any())).thenReturn(Collections.emptySet()); + + underTest.load(); + + verify(serviceLoaderWrapper).load(CoreExtensionsLoader.class.getClassLoader()); + verify(coreExtensionRepository).setLoadedCoreExtensions(Collections.emptySet()); + verifyNoMoreInteractions(serviceLoaderWrapper, coreExtensionRepository); + } + + @Test + public void load_sets_loaded_core_extensions_into_repository() { + Set coreExtensions = IntStream.range(0, 1 + new Random().nextInt(5)) + .mapToObj(i -> newCoreExtension("core_ext_" + i)) + .collect(Collectors.toSet()); + when(serviceLoaderWrapper.load(any())).thenReturn(coreExtensions); + + underTest.load(); + + verify(serviceLoaderWrapper).load(CoreExtensionsLoader.class.getClassLoader()); + verify(coreExtensionRepository).setLoadedCoreExtensions(coreExtensions); + verifyNoMoreInteractions(serviceLoaderWrapper, coreExtensionRepository); + } + + @Test + public void load_fails_with_ISE_if_multiple_core_extensions_declare_same_name() { + Set coreExtensions = ImmutableSet.of(newCoreExtension("a"), newCoreExtension("a")); + when(serviceLoaderWrapper.load(any())).thenReturn(coreExtensions); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Multiple core extensions declare the following names: a"); + + underTest.load(); + } + + @Test + public void load_fails_with_ISE_if_multiple_core_extensions_declare_same_names() { + Set coreExtensions = ImmutableSet.of(newCoreExtension("a"), newCoreExtension("a"), newCoreExtension("b"), newCoreExtension("b")); + when(serviceLoaderWrapper.load(any())).thenReturn(coreExtensions); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Multiple core extensions declare the following names: a, b"); + + underTest.load(); + } + + private static CoreExtension newCoreExtension(String name) { + CoreExtension res = mock(CoreExtension.class); + when(res.getName()).thenReturn(name); + return res; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java index 1aea964a478..bc49833ec6f 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java @@ -32,6 +32,9 @@ import org.sonar.api.utils.UriReader; import org.sonar.api.utils.Version; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; +import org.sonar.core.extension.CoreExtensionRepositoryImpl; +import org.sonar.core.extension.CoreExtensionsInstaller; +import org.sonar.core.extension.CoreExtensionsLoader; import org.sonar.core.platform.ComponentContainer; import org.sonar.core.platform.PluginClassloaderFactory; import org.sonar.core.platform.PluginInfo; @@ -39,6 +42,7 @@ import org.sonar.core.platform.PluginLoader; import org.sonar.core.platform.PluginRepository; import org.sonar.core.util.DefaultHttpDownloader; import org.sonar.core.util.UuidFactoryImpl; +import org.sonar.scanner.extension.ScannerCoreExtensionsInstaller; import org.sonar.scanner.platform.DefaultServer; import org.sonar.scanner.repository.DefaultMetricsRepositoryLoader; import org.sonar.scanner.repository.MetricsRepositoryLoader; @@ -99,6 +103,7 @@ public class GlobalContainer extends ComponentContainer { new MetricsRepositoryProvider(), UuidFactoryImpl.INSTANCE); addIfMissing(ScannerPluginInstaller.class, PluginInstaller.class); + add(CoreExtensionRepositoryImpl.class, CoreExtensionsLoader.class, ScannerCoreExtensionsInstaller.class); addIfMissing(DefaultSettingsLoader.class, SettingsLoader.class); addIfMissing(DefaultMetricsRepositoryLoader.class, MetricsRepositoryLoader.class); } @@ -106,6 +111,7 @@ public class GlobalContainer extends ComponentContainer { @Override protected void doAfterStart() { installPlugins(); + loadCoreExtensions(); } private void installPlugins() { @@ -116,6 +122,11 @@ public class GlobalContainer extends ComponentContainer { } } + private void loadCoreExtensions() { + CoreExtensionsLoader loader = getComponentByType(CoreExtensionsLoader.class); + loader.load(); + } + public void executeTask(Map taskProperties, Object... components) { long startTime = System.currentTimeMillis(); new TaskContainer(this, taskProperties, components).execute(); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstaller.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstaller.java new file mode 100644 index 00000000000..e1b2f49aa8a --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstaller.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.scanner.extension; + +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.ScannerSide; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.extension.CoreExtensionsInstaller; + +@ScannerSide +public class ScannerCoreExtensionsInstaller extends CoreExtensionsInstaller { + public ScannerCoreExtensionsInstaller(SonarRuntime sonarRuntime, CoreExtensionRepository coreExtensionRepository) { + super(sonarRuntime, coreExtensionRepository, ScannerSide.class); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/package-info.java new file mode 100644 index 00000000000..eb9aa1ab845 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.scanner.extension; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java index f00208dd652..af03f0ae932 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java @@ -21,17 +21,16 @@ package org.sonar.scanner.scan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.batch.fs.internal.FileMetadata; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.issue.NoSonarFilter; import org.sonar.api.resources.Project; import org.sonar.api.scan.filesystem.FileExclusions; +import org.sonar.core.extension.CoreExtensionsInstaller; import org.sonar.core.platform.ComponentContainer; import org.sonar.scanner.DefaultFileLinesContextFactory; import org.sonar.scanner.bootstrap.ExtensionInstaller; -import org.sonar.scanner.bootstrap.ExtensionUtils; import org.sonar.scanner.bootstrap.GlobalAnalysisMode; import org.sonar.scanner.bootstrap.ScannerExtensionDictionnary; import org.sonar.scanner.deprecated.DeprecatedSensorContext; @@ -73,6 +72,10 @@ import org.sonar.scanner.sensor.SensorOptimizer; import org.sonar.scanner.source.HighlightableBuilder; import org.sonar.scanner.source.SymbolizableBuilder; +import static org.sonar.api.batch.InstantiationStrategy.PER_PROJECT; +import static org.sonar.scanner.bootstrap.ExtensionUtils.isInstantiationStrategy; +import static org.sonar.scanner.bootstrap.ExtensionUtils.isScannerSide; + public class ModuleScanContainer extends ComponentContainer { private static final Logger LOG = LoggerFactory.getLogger(ModuleScanContainer.class); private final DefaultInputModule module; @@ -165,8 +168,10 @@ public class ModuleScanContainer extends ComponentContainer { } private void addExtensions() { - ExtensionInstaller installer = getComponentByType(ExtensionInstaller.class); - installer.install(this, e -> ExtensionUtils.isScannerSide(e) && ExtensionUtils.isInstantiationStrategy(e, InstantiationStrategy.PER_PROJECT)); + ExtensionInstaller pluginInstaller = getComponentByType(ExtensionInstaller.class); + pluginInstaller.install(this, e -> isScannerSide(e) && isInstantiationStrategy(e, PER_PROJECT)); + CoreExtensionsInstaller coreExtensionsInstaller = getComponentByType(CoreExtensionsInstaller.class); + coreExtensionsInstaller.install(this, t -> isInstantiationStrategy(t, PER_PROJECT)); } @Override diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java index af9ea941f51..c0ab266b34f 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java @@ -22,7 +22,6 @@ package org.sonar.scanner.scan; import com.google.common.annotations.VisibleForTesting; import javax.annotation.Nullable; import org.sonar.api.CoreProperties; -import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.batch.fs.internal.InputModuleHierarchy; import org.sonar.api.batch.fs.internal.SensorStrategy; @@ -33,6 +32,7 @@ import org.sonar.api.scan.filesystem.PathResolver; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.core.config.ScannerProperties; +import org.sonar.core.extension.CoreExtensionsInstaller; import org.sonar.core.metric.ScannerMetrics; import org.sonar.core.platform.ComponentContainer; import org.sonar.scanner.ProjectAnalysisInfo; @@ -41,7 +41,6 @@ import org.sonar.scanner.analysis.AnalysisTempFolderProvider; import org.sonar.scanner.analysis.DefaultAnalysisMode; import org.sonar.scanner.bootstrap.ExtensionInstaller; import org.sonar.scanner.bootstrap.ExtensionMatcher; -import org.sonar.scanner.bootstrap.ExtensionUtils; import org.sonar.scanner.bootstrap.GlobalAnalysisMode; import org.sonar.scanner.bootstrap.MetricProvider; import org.sonar.scanner.cpd.CpdExecutor; @@ -102,6 +101,10 @@ import org.sonar.scanner.scan.measure.MeasureCache; import org.sonar.scanner.scm.ScmChangedFilesProvider; import org.sonar.scanner.storage.Storages; +import static org.sonar.api.batch.InstantiationStrategy.PER_BATCH; +import static org.sonar.scanner.bootstrap.ExtensionUtils.isInstantiationStrategy; +import static org.sonar.scanner.bootstrap.ExtensionUtils.isScannerSide; + public class ProjectScanContainer extends ComponentContainer { private static final Logger LOG = Loggers.get(ProjectScanContainer.class); @@ -238,7 +241,15 @@ public class ProjectScanContainer extends ComponentContainer { } private void addBatchExtensions() { - getComponentByType(ExtensionInstaller.class).install(this, new BatchExtensionFilter()); + getComponentByType(ExtensionInstaller.class) + .install(this, getBatchPluginExtensionsFilter()); + getComponentByType(CoreExtensionsInstaller.class) + .install(this, extension -> isInstantiationStrategy(extension, PER_BATCH)); + } + + @VisibleForTesting + static ExtensionMatcher getBatchPluginExtensionsFilter() { + return extension -> isScannerSide(extension) && isInstantiationStrategy(extension, PER_BATCH); } @Override @@ -301,12 +312,4 @@ public class ProjectScanContainer extends ComponentContainer { new ModuleScanContainer(this, module, analysisMode).execute(); } - static class BatchExtensionFilter implements ExtensionMatcher { - @Override - public boolean accept(Object extension) { - return ExtensionUtils.isScannerSide(extension) - && ExtensionUtils.isInstantiationStrategy(extension, InstantiationStrategy.PER_BATCH); - } - } - } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/task/TaskContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/task/TaskContainer.java index e863148e9e8..4d81fa18127 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/task/TaskContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/task/TaskContainer.java @@ -22,16 +22,18 @@ package org.sonar.scanner.task; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.sonar.api.CoreProperties; -import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.task.Task; import org.sonar.api.task.TaskDefinition; import org.sonar.api.utils.MessageException; +import org.sonar.core.extension.CoreExtensionsInstaller; import org.sonar.core.platform.ComponentContainer; import org.sonar.scanner.bootstrap.ExtensionInstaller; -import org.sonar.scanner.bootstrap.ExtensionMatcher; -import org.sonar.scanner.bootstrap.ExtensionUtils; import org.sonar.scanner.bootstrap.GlobalProperties; +import static org.sonar.api.batch.InstantiationStrategy.PER_TASK; +import static org.sonar.scanner.bootstrap.ExtensionUtils.isInstantiationStrategy; +import static org.sonar.scanner.bootstrap.ExtensionUtils.isScannerSide; + public class TaskContainer extends ComponentContainer { private final Map taskProperties; @@ -57,15 +59,10 @@ public class TaskContainer extends ComponentContainer { } private void addTaskExtensions() { - getComponentByType(ExtensionInstaller.class).install(this, new TaskExtensionFilter()); - } - - static class TaskExtensionFilter implements ExtensionMatcher { - @Override - public boolean accept(Object extension) { - return ExtensionUtils.isScannerSide(extension) - && ExtensionUtils.isInstantiationStrategy(extension, InstantiationStrategy.PER_TASK); - } + getComponentByType(ExtensionInstaller.class) + .install(this, extension -> isScannerSide(extension) && isInstantiationStrategy(extension, PER_TASK)); + getComponentByType(CoreExtensionsInstaller.class) + .install(this, t -> isInstantiationStrategy(t, PER_TASK)); } @Override diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstallerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstallerTest.java new file mode 100644 index 00000000000..a359ba30967 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstallerTest.java @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.scanner.extension; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.stream.Stream; +import org.junit.Test; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.ScannerSide; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; +import org.sonar.core.extension.CoreExtension; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ScannerCoreExtensionsInstallerTest { + private SonarRuntime sonarRuntime = mock(SonarRuntime.class); + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + + private ScannerCoreExtensionsInstaller underTest = new ScannerCoreExtensionsInstaller(sonarRuntime, coreExtensionRepository); + + @Test + public void install_only_adds_ScannerSide_annotated_extension_to_container() { + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of( + new CoreExtension() { + @Override + public String getName() { + return "foo"; + } + + @Override + public void load(Context context) { + context.addExtensions(CeClass.class, ScannerClass.class, WebServerClass.class, + NoAnnotationClass.class, OtherAnnotationClass.class, MultipleAnnotationClass.class); + } + })); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, t -> true); + + assertThat(container.getPicoContainer().getComponentAdapters()) + .hasSize(ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2); + assertThat(container.getComponentByType(ScannerClass.class)).isNotNull(); + assertThat(container.getComponentByType(MultipleAnnotationClass.class)).isNotNull(); + } + + @ComputeEngineSide + public static final class CeClass { + + } + + @ServerSide + public static final class WebServerClass { + + } + + @ScannerSide + public static final class ScannerClass { + + } + + @ServerSide + @ComputeEngineSide + @ScannerSide + public static final class MultipleAnnotationClass { + + } + + public static final class NoAnnotationClass { + + } + + @DarkSide + public static final class OtherAnnotationClass { + + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface DarkSide { + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectScanContainerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectScanContainerTest.java index dd1a77c3d26..8ca3ebf301f 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectScanContainerTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectScanContainerTest.java @@ -24,6 +24,7 @@ import org.sonar.api.BatchExtension; import org.sonar.api.ServerExtension; import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.task.TaskExtension; +import org.sonar.scanner.bootstrap.ExtensionMatcher; import static org.assertj.core.api.Assertions.assertThat; @@ -31,7 +32,7 @@ public class ProjectScanContainerTest { @Test public void should_add_only_batch_extensions() { - ProjectScanContainer.BatchExtensionFilter filter = new ProjectScanContainer.BatchExtensionFilter(); + ExtensionMatcher filter = ProjectScanContainer.getBatchPluginExtensionsFilter(); assertThat(filter.accept(new MyBatchExtension())).isTrue(); assertThat(filter.accept(MyBatchExtension.class)).isTrue(); -- 2.39.5