Browse Source

SONAR-10690 add Core Extension support in SonarQube

tags/7.5
Sébastien Lesaint 6 years ago
parent
commit
ded90fa8ef
49 changed files with 2161 additions and 110 deletions
  1. 12
    2
      server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
  2. 32
    0
      server/sonar-ce/src/main/java/org/sonar/ce/platform/CECoreExtensionsInstaller.java
  3. 2
    2
      server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
  4. 109
    0
      server/sonar-ce/src/test/java/org/sonar/ce/platform/CECoreExtensionsInstallerTest.java
  5. 34
    0
      server/sonar-server-common/build.gradle
  6. 58
    0
      server/sonar-server-common/src/main/java/org/sonar/server/l18n/ServerI18n.java
  7. 93
    0
      server/sonar-server-common/src/test/java/org/sonar/server/l18n/ServerI18nTest.java
  8. 1
    0
      server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle.properties
  9. 1
    0
      server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle_fr.properties
  10. 2
    0
      server/sonar-server-common/src/test/resources/org/sonar/l10n/core_fr.properties
  11. 1
    0
      server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext.properties
  12. 1
    0
      server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext_fr.properties
  13. 1
    0
      server/sonar-server/build.gradle
  14. 32
    0
      server/sonar-server/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java
  15. 7
    2
      server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java
  16. 8
    1
      server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  17. 1
    8
      server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
  18. 25
    10
      server/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java
  19. 60
    15
      server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java
  20. 37
    0
      server/sonar-server/src/main/java/org/sonar/server/ui/page/CorePageDefinition.java
  21. 24
    0
      server/sonar-server/src/main/java/org/sonar/server/ui/page/package-info.java
  22. 109
    0
      server/sonar-server/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java
  23. 72
    22
      server/sonar-server/src/test/java/org/sonar/server/plugins/StaticResourcesServletTest.java
  24. 9
    7
      server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java
  25. 4
    1
      server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java
  26. 4
    1
      server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java
  27. 4
    1
      server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java
  28. 4
    1
      server/sonar-server/src/test/java/org/sonar/server/ui/ws/SettingsActionTest.java
  29. 1
    0
      settings.gradle
  30. 48
    0
      sonar-core/src/main/java/org/sonar/core/extension/CoreExtension.java
  31. 57
    0
      sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepository.java
  32. 70
    0
      sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepositoryImpl.java
  33. 166
    0
      sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsInstaller.java
  34. 78
    0
      sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsLoader.java
  35. 37
    0
      sonar-core/src/main/java/org/sonar/core/extension/ExtensionProviderSupport.java
  36. 24
    0
      sonar-core/src/main/java/org/sonar/core/extension/package-info.java
  37. 7
    7
      sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java
  38. 18
    2
      sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java
  39. 177
    0
      sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionRepositoryImplTest.java
  40. 424
    0
      sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsInstallerTest.java
  41. 98
    0
      sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsLoaderTest.java
  42. 11
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java
  43. 32
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstaller.java
  44. 23
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/package-info.java
  45. 9
    4
      sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java
  46. 14
    11
      sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java
  47. 9
    12
      sonar-scanner-engine/src/main/java/org/sonar/scanner/task/TaskContainer.java
  48. 109
    0
      sonar-scanner-engine/src/test/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstallerTest.java
  49. 2
    1
      sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectScanContainerTest.java

+ 12
- 2
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java View 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,


+ 32
- 0
server/sonar-ce/src/main/java/org/sonar/ce/platform/CECoreExtensionsInstaller.java View File

@@ -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);
}
}

+ 2
- 2
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java View 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

+ 109
- 0
server/sonar-ce/src/test/java/org/sonar/ce/platform/CECoreExtensionsInstallerTest.java View File

@@ -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 {
}
}

+ 34
- 0
server/sonar-server-common/build.gradle View File

@@ -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
}
}
}

+ 58
- 0
server/sonar-server-common/src/main/java/org/sonar/server/l18n/ServerI18n.java View File

@@ -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);
}
}

+ 93
- 0
server/sonar-server-common/src/test/java/org/sonar/server/l18n/ServerI18nTest.java View File

@@ -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;
}

}

+ 1
- 0
server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle.properties View File

@@ -0,0 +1 @@
checkstyle.rule1.name=Rule one

+ 1
- 0
server/sonar-server-common/src/test/resources/org/sonar/l10n/checkstyle_fr.properties View File

@@ -0,0 +1 @@
checkstyle.rule1.name=Rule un

+ 2
- 0
server/sonar-server-common/src/test/resources/org/sonar/l10n/core_fr.properties View File

@@ -0,0 +1,2 @@
any=Tous
empty=

+ 1
- 0
server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext.properties View File

@@ -0,0 +1 @@
coreext.rule1.name=Rule one

+ 1
- 0
server/sonar-server-common/src/test/resources/org/sonar/l10n/coreext_fr.properties View File

@@ -0,0 +1 @@
coreext.rule1.name=Rule un

+ 1
- 0
server/sonar-server/build.gradle View 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')) {

+ 32
- 0
server/sonar-server/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java View File

@@ -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);
}
}

+ 7
- 2
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java View File

@@ -20,10 +20,12 @@
package org.sonar.server.platform.platformlevel;

import org.sonar.api.utils.Durations;
import org.sonar.core.i18n.DefaultI18n;
import org.sonar.core.i18n.RuleI18nManager;
import org.sonar.core.platform.PluginClassloaderFactory;
import org.sonar.core.platform.PluginLoader;
import org.sonar.core.extension.CoreExtensionRepositoryImpl;
import org.sonar.core.extension.CoreExtensionsLoader;
import org.sonar.server.l18n.ServerI18n;
import org.sonar.server.platform.DatabaseServerCompatibility;
import org.sonar.server.platform.DefaultServerUpgradeStatus;
import org.sonar.server.platform.StartupMetadataProvider;
@@ -70,9 +72,11 @@ public class PlatformLevel2 extends PlatformLevel {
PluginClassloaderFactory.class,
InstalledPluginReferentialFactory.class,
WebServerExtensionInstaller.class,
CoreExtensionRepositoryImpl.class,
CoreExtensionsLoader.class,

// depends on plugins
DefaultI18n.class,
ServerI18n.class,
RuleI18nManager.class);

// Migration state must be kept at level2 to survive moving in and then out of safe mode
@@ -95,6 +99,7 @@ public class PlatformLevel2 extends PlatformLevel {
public PlatformLevel start() {
// ensuring the HistoryTable exists must be the first thing done when this level is started
getOptional(MigrationHistoryTable.class).ifPresent(MigrationHistoryTable::start);
get(CoreExtensionsLoader.class).load();
return super.start();
}
}

+ 8
- 1
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View 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();


+ 1
- 8
server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java View 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);
}
}

+ 25
- 10
server/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java View 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) {

+ 60
- 15
server/sonar-server/src/main/java/org/sonar/server/ui/PageRepository.java View File

@@ -20,10 +20,8 @@
package org.sonar.server.ui;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.sonar.api.Startable;
import org.sonar.api.server.ServerSide;
@@ -33,8 +31,11 @@ import org.sonar.api.web.page.Page.Qualifier;
import org.sonar.api.web.page.Page.Scope;
import org.sonar.api.web.page.PageDefinition;
import org.sonar.core.platform.PluginRepository;
import org.sonar.core.extension.CoreExtensionRepository;
import org.sonar.server.ui.page.CorePageDefinition;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.copyOf;
import static java.util.Collections.emptyList;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
@@ -46,26 +47,66 @@ import static org.sonar.core.util.stream.MoreCollectors.toList;
@ServerSide
public class PageRepository implements Startable {
private final PluginRepository pluginRepository;
private final CoreExtensionRepository coreExtensionRepository;
private final List<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);
}

}

+ 37
- 0
server/sonar-server/src/main/java/org/sonar/server/ui/page/CorePageDefinition.java View File

@@ -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();
}

+ 24
- 0
server/sonar-server/src/main/java/org/sonar/server/ui/page/package-info.java View File

@@ -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;


+ 109
- 0
server/sonar-server/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java View File

@@ -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 {
}

}

+ 72
- 22
server/sonar-server/src/test/java/org/sonar/server/plugins/StaticResourcesServletTest.java View 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

+ 9
- 7
server/sonar-server/src/test/java/org/sonar/server/ui/PageRepositoryTest.java View 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");

+ 4
- 1
server/sonar-server/src/test/java/org/sonar/server/ui/ws/ComponentActionTest.java View 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);
}

+ 4
- 1
server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java View 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);
}

+ 4
- 1
server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java View 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);
}

+ 4
- 1
server/sonar-server/src/test/java/org/sonar/server/ui/ws/SettingsActionTest.java View 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);
}

+ 1
- 0
settings.gradle View 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'


+ 48
- 0
sonar-core/src/main/java/org/sonar/core/extension/CoreExtension.java View File

@@ -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);
}

+ 57
- 0
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepository.java View File

@@ -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);
}

+ 70
- 0
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionRepositoryImpl.java View File

@@ -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");
}
}

+ 166
- 0
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsInstaller.java View File

@@ -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;
}
}
}

+ 78
- 0
sonar-core/src/main/java/org/sonar/core/extension/CoreExtensionsLoader.java View File

@@ -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());
}
}
}

+ 37
- 0
sonar-core/src/main/java/org/sonar/core/extension/ExtensionProviderSupport.java View File

@@ -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);
}
}

+ 24
- 0
sonar-core/src/main/java/org/sonar/core/extension/package-info.java View File

@@ -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;


+ 7
- 7
sonar-core/src/main/java/org/sonar/core/i18n/DefaultI18n.java View 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);

+ 18
- 2
sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java View 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) {

+ 177
- 0
sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionRepositoryImplTest.java View File

@@ -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");
}
};
}
}

+ 424
- 0
sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsInstallerTest.java View File

@@ -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 {

}

}

+ 98
- 0
sonar-core/src/test/java/org/sonar/core/extension/CoreExtensionsLoaderTest.java View File

@@ -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;
}
}

+ 11
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalContainer.java View 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();

+ 32
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstaller.java View File

@@ -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);
}
}

+ 23
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/extension/package-info.java View File

@@ -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;

+ 9
- 4
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ModuleScanContainer.java View 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

+ 14
- 11
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java View 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);
}
}

}

+ 9
- 12
sonar-scanner-engine/src/main/java/org/sonar/scanner/task/TaskContainer.java View 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

+ 109
- 0
sonar-scanner-engine/src/test/java/org/sonar/scanner/extension/ScannerCoreExtensionsInstallerTest.java View File

@@ -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 {
}

}

+ 2
- 1
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectScanContainerTest.java View 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();

Loading…
Cancel
Save