]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10690 add Core Extension support in SonarQube
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 25 May 2018 09:23:29 +0000 (11:23 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 12 Jun 2018 18:20:59 +0000 (20:20 +0200)
49 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/main/java/org/sonar/ce/platform/CECoreExtensionsInstaller.java [new file with mode: 0644]
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-ce/src/test/java/org/sonar/ce/platform/CECoreExtensionsInstallerTest.java [new file with mode: 0644]
server/sonar-server-common/build.gradle [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/l18n/ServerI18n.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/l18n/ServerI18nTest.java [new file with mode: 0644]
server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle.properties [new file with mode: 0644]
server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle_fr.properties [new file with mode: 0644]
server/sonar-server-common/src/test/resources/org/sonar/l10n/core_fr.properties [new file with mode: 0644]
server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext.properties [new file with mode: 0644]
server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext_fr.properties [new file with mode: 0644]
server/sonar-server/build.gradle
server/sonar-server/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
server/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java
server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java
server/sonar-server/src/main/java/org/sonar/server/ui/page/CorePageDefinition.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ui/page/package-info.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/plugins/StaticResourcesServletTest.java
server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java
server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java
server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java
server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java
server/sonar-server/src/test/java/org/sonar/server/ui/ws/SettingsActionTest.java
settings.gradle
sonar-core/src/main/java/org/sonar/core/extension/CoreExtension.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepository.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepositoryImpl.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsInstaller.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsLoader.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/extension/ExtensionProviderSupport.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/extension/package-info.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java
sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java
sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionRepositoryImplTest.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsInstallerTest.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsLoaderTest.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstaller.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/package-info.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/task/TaskContainer.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstallerTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectScanContainerTest.java

index e8ef7188e6a0a3b94549cf6b240db162a0a277fd..bfc640055d9ed18311d3792e241da10895fb4148 100644 (file)
@@ -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 (file)
index 0000000..607726c
--- /dev/null
@@ -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);
+  }
+}
index e9c72239901e661593c5232217a0ef7a597fd312..b079bae241e2322cb0703fe9d44ab7838ce8f647 100644 (file)
@@ -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 (file)
index 0000000..df07584
--- /dev/null
@@ -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 (file)
index 0000000..12184b6
--- /dev/null
@@ -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 (file)
index 0000000..0f5c556
--- /dev/null
@@ -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 (file)
index 0000000..608b2f3
--- /dev/null
@@ -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<PluginInfo> plugins = singletonList(newPlugin("checkstyle"));
+    when(pluginRepository.getPluginInfos()).thenReturn(plugins);
+
+    CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class);
+    Stream<CoreExtension> 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 (file)
index 0000000..10fa929
--- /dev/null
@@ -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 (file)
index 0000000..b2fc8f9
--- /dev/null
@@ -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 (file)
index 0000000..9b473d0
--- /dev/null
@@ -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 (file)
index 0000000..e84fc7c
--- /dev/null
@@ -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 (file)
index 0000000..0cd4598
--- /dev/null
@@ -0,0 +1 @@
+coreext.rule1.name=Rule un
index 78bf82df66261b4826eab4a676b8a97bbfeab174..6b4fd8a88a6059166a9a9ceb6d1fadf888c814c2 100644 (file)
@@ -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 (file)
index 0000000..7ab01bc
--- /dev/null
@@ -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);
+  }
+}
index ca1ef1020e52d169b7f39bb2ecc978cbebeb8eb9..b32ad3d14e9581b677e9be158f1b1237b366b36e 100644 (file)
 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();
   }
 }
index e9988fbc7c9084df10f2614a29dfbdcfc0f39577..928da0fd9fab724ab7295576b9e91be2e9ecaac4 100644 (file)
@@ -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();
 
index ae94ead30eb47724a6261cac870bd29addff4c34..16bcba75ca9f8e2d54f64bbe2ea21de98f6732fc 100644 (file)
@@ -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);
-  }
 }
index 3e5ad22e85bca1ff878b9d8b98f2bcef810ec279..2017eb7c693256c56c310ccf1d0e34dc15cf2bff 100644 (file)
@@ -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) {
index 0929a36d4380564851262382a72b5c36f85c01f0..f16dc84394a872d4ea800f78105a7769222f6d10 100644 (file)
 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<PageDefinition> definitions;
+  private final List<CorePageDefinition> corePageDefinitions;
   private List<Page> 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<Page> 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 (file)
index 0000000..595cd08
--- /dev/null
@@ -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).
+ * <p>
+ * 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 (file)
index 0000000..239574f
--- /dev/null
@@ -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 (file)
index 0000000..60eee2c
--- /dev/null
@@ -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 {
+  }
+
+}
index fad36be82144d4e8c4e4e5a6ed21c76826a720db..d3b79457c9f1a1a06570723243a388d3933848e0 100644 (file)
@@ -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
index 89165d859903d56013182adb030148b1e5cefd16..ae5b61f49f8795eb23e4a8f3b581d35143654fc7 100644 (file)
@@ -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<Page> 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<Page> 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<Page> 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<Page> 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<Page> 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");
index 7ddcd2b32c061191158562b299bc1cec5165604b..c214196f65c7691a20dfe1087bea2dc24ec590ed 100644 (file)
@@ -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);
       }
index 186264842f83ec6f50604bc5be3ddb0cc600f34e..264155932787a936cd595b10a925cd8b675597a6 100644 (file)
@@ -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);
       }
index 986475ca37bad01e14beaf4306aa8805b9bd35a4..e49187b73827e5960e55f22b3bd27044fd7d45be 100644 (file)
@@ -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);
       }
index 2b4034c6b246d62cd19a33c3d56db4c1148ce595..14085068fbf756c1a043c911f95ed58e8aa8c5e0 100644 (file)
@@ -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);
       }
index ff20a6df26718961cffa4a3d4991ce7b97251a05..d15f0d6f31c77868927664e2a968d592e5c1e4ec 100644 (file)
@@ -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 (file)
index 0000000..c8279d8
--- /dev/null
@@ -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.
+   * <p>
+   * 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);
+
+    <T> Context addExtensions(Collection<T> 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 (file)
index 0000000..9369bf5
--- /dev/null
@@ -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<CoreExtension> 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<CoreExtension> 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 (file)
index 0000000..5a87e47
--- /dev/null
@@ -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<CoreExtension> coreExtensions = null;
+  private Set<CoreExtension> installedCoreExtensions = null;
+
+  @Override
+  public void setLoadedCoreExtensions(Set<CoreExtension> coreExtensions) {
+    checkState(this.coreExtensions == null, "Repository has already been initialized");
+
+    this.coreExtensions = ImmutableSet.copyOf(coreExtensions);
+    this.installedCoreExtensions = new HashSet<>(coreExtensions.size());
+  }
+
+  @Override
+  public Stream<CoreExtension> 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 (file)
index 0000000..a9b175f
--- /dev/null
@@ -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<? extends Annotation> supportedAnnotationType;
+
+  protected CoreExtensionsInstaller(SonarRuntime sonarRuntime, CoreExtensionRepository coreExtensionRepository,
+    Class<? extends Annotation> supportedAnnotationType) {
+    this.sonarRuntime = sonarRuntime;
+    this.coreExtensionRepository = coreExtensionRepository;
+    this.supportedAnnotationType = supportedAnnotationType;
+  }
+
+  public void install(ComponentContainer container, Predicate<Object> extensionFilter) {
+    coreExtensionRepository.loadedCoreExtensions()
+      .forEach(coreExtension -> install(container, extensionFilter, coreExtension));
+  }
+
+  private void install(ComponentContainer container, Predicate<Object> extensionFilter, CoreExtension coreExtension) {
+    String coreExtensionName = coreExtension.getName();
+    try {
+      List<Object> 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<Object> addDeclaredExtensions(ComponentContainer container, Predicate<Object> extensionFilter,
+    CoreExtension coreExtension) {
+    ContextImpl context = new ContextImpl(container, extensionFilter, coreExtension.getName());
+    coreExtension.load(context);
+    return context.getProviders();
+  }
+
+  private void addProvidedExtensions(ComponentContainer container, Predicate<Object> extensionFilter,
+    String extensionCategory, List<Object> providerKeys) {
+    providerKeys.stream()
+      .map(providerKey -> (ExtensionProvider) container.getComponentByKey(providerKey))
+      .forEach(provider -> addFromProvider(container, extensionFilter, extensionCategory, provider));
+  }
+
+  private void addFromProvider(ComponentContainer container, Predicate<Object> 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 <T> boolean addSupportedExtension(ComponentContainer container, Predicate<Object> extensionFilter,
+    String extensionCategory, T component) {
+    if (hasSupportedAnnotation(component) && extensionFilter.test(component)) {
+      container.addExtension(extensionCategory, component);
+      return true;
+    }
+    return false;
+  }
+
+  private <T> boolean hasSupportedAnnotation(T component) {
+    return AnnotationUtils.getAnnotation(component, supportedAnnotationType) != null;
+  }
+
+  private class ContextImpl implements CoreExtension.Context {
+    private final ComponentContainer container;
+    private final Predicate<Object> extensionFilter;
+    private final String extensionCategory;
+    private final List<Object> providers = new ArrayList<>();
+
+    public ContextImpl(ComponentContainer container, Predicate<Object> 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 <T> CoreExtension.Context addExtensions(Collection<T> components) {
+      requireNonNull(components, "components can't be null");
+      components.forEach(this::addExtension);
+      return this;
+    }
+
+    public List<Object> 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 (file)
index 0000000..79d78cb
--- /dev/null
@@ -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<CoreExtension> 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<CoreExtension> coreExtensions) {
+    Map<String, Long> nameCounts = coreExtensions.stream()
+      .map(CoreExtension::getName)
+      .collect(Collectors.groupingBy(t -> t, Collectors.counting()));
+    Set<String> 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<CoreExtension> load(ClassLoader classLoader) {
+      ServiceLoader<CoreExtension> 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 (file)
index 0000000..bc0fae8
--- /dev/null
@@ -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 (file)
index 0000000..49ad268
--- /dev/null
@@ -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;
+
index fcdff6c4f1ae0be43b12b7b1caca13b1293f37a2..c181cfbe7c05a9825fad40120b2585af936ab0c4 100644 (file)
@@ -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);
index 0bcb93396b9f6f37220ae1b0bab3f72f92a11520..7454777c7a0c71c038adcc85e44c53fb10a862b9 100644 (file)
@@ -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 (file)
index 0000000..6764e34
--- /dev/null
@@ -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<CoreExtension> 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<CoreExtension> 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<CoreExtension> 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<CoreExtension> 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<CoreExtension> 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 (file)
index 0000000..d9d0337
--- /dev/null
@@ -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<CoreExtension.Context> 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<CoreExtension> 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<CoreExtension.Context, Collection<Object>> extensionAdder) {
+    List<Object> 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<CoreExtension.Context, Collection<Object>> extensionAdder) {
+    List<Object> 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<CoreExtension.Context, Collection<Object>> extensionAdder) {
+    List<Object> 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<CoreExtension.Context, Collection<Object>> extensionAdder) {
+    List<Object> 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<CoreExtension.Context, Collection<Object>> extensionAdder) {
+    PropertyDefinition propertyDefinitionNoCategory = PropertyDefinition.builder("fooKey").build();
+    PropertyDefinition propertyDefinitionWithCategory = PropertyDefinition.builder("barKey").category("donut").build();
+    List<Object> 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<CoreExtension.Context, Collection<Object>> extensionAdder) {
+    List<Object> 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<CoreExtension.Context, Collection<Object>> addExtension = (context, objects) -> objects.forEach(context::addExtension);
+    BiConsumer<CoreExtension.Context, Collection<Object>> 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<CoreExtension.Context, Collection<Object>> addExtensions = CoreExtension.Context::addExtensions;
+    return new Object[][] {
+      {addExtension},
+      {addExtensions},
+      {addExtensionsVarArg}
+    };
+  }
+
+  private static void assertAddedExtensions(ComponentContainer container, int addedExtensions) {
+    Collection<ComponentAdapter<?>> adapters = container.getPicoContainer().getComponentAdapters();
+    assertThat(adapters)
+      .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + addedExtensions);
+  }
+
+  private static void assertAddedExtensions(ComponentContainer container, Class... classes) {
+    Collection<ComponentAdapter<?>> adapters = container.getPicoContainer().getComponentAdapters();
+    assertThat(adapters)
+      .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + classes.length);
+
+    Stream<Class> 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<CoreExtension.Context> 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 (file)
index 0000000..180592e
--- /dev/null
@@ -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<CoreExtension> 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<CoreExtension> 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<CoreExtension> 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;
+  }
+}
index 1aea964a4782dccfb35dd6d4c535efe502502bfb..bc49833ec6f87ee1edbe158a443785ffada22444 100644 (file)
@@ -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<String, String> 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 (file)
index 0000000..e1b2f49
--- /dev/null
@@ -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 (file)
index 0000000..eb9aa1a
--- /dev/null
@@ -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;
index f00208dd652e4e4e6f435bf7aefd354565e4c427..af03f0ae9329861d8e037d25bb2d42c8d23707ed 100644 (file)
@@ -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
index af9ea941f51dad5f60e076fe89a71b36143467d1..c0ab266b34fe103311ef899c03419f8ff4b4551f 100644 (file)
@@ -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);
-    }
-  }
-
 }
index e863148e9e8637b610220035a29d9e9a1f1a79ea..4d81fa1812720c10a889fe8a6fd07f04a195508c 100644 (file)
@@ -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<String, String> 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 (file)
index 0000000..a359ba3
--- /dev/null
@@ -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 {
+  }
+
+}
index dd1a77c3d268d69d8d99fe116dfe10bfbfd5fa40..8ca3ebf301f9c94d72653a45d94f3f5230c3fb1d 100644 (file)
@@ -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();