aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-api
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-08-14 12:46:39 +0200
committerSonarTech <sonartech@sonarsource.com>2019-08-14 20:21:16 +0200
commit314809259ca1540d1e015582d2263daebfa5f3a5 (patch)
treecb0e2992664442d521dd66d9b4e584d5dee192e4 /server/sonar-webserver-api
parent23e09ca2d312815ddc9c05cd83ca584670250fd5 (diff)
downloadsonarqube-314809259ca1540d1e015582d2263daebfa5f3a5.tar.gz
sonarqube-314809259ca1540d1e015582d2263daebfa5f3a5.zip
rename sonar-webserver-common to sonar-webserver-api
Diffstat (limited to 'server/sonar-webserver-api')
-rw-r--r--server/sonar-webserver-api/build.gradle58
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapper.java43
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapperImpl.java95
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolder.java42
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolderImpl.java41
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/app/package-info.java24
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeature.java26
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureExtension.java30
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxy.java29
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxyImpl.java38
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/branch/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/CeHttpClient.java30
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/BadRequestException.java75
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ForbiddenException.java34
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/Message.java66
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/NotFoundException.java65
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ServerException.java35
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java32
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/package-info.java24
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/health/ClusterHealth.java78
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/health/Health.java136
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/health/HealthChecker.java35
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/health/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/platform/ClusterFeature.java31
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/platform/Platform.java34
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/platform/SystemInfoWriter.java26
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/platform/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java81
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java165
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java124
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java90
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java68
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java372
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java113
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java55
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/WebServerExtensionInstaller.java33
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/EditionBundledPlugins.java44
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/package-info.java24
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java41
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java55
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java91
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/project/RekeyedProject.java68
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/project/Visibility.java72
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/project/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/ProjectsInWarning.java44
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java101
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java66
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java29
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java180
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java24
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/BulkChangeResult.java60
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/QProfileRules.java64
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java91
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoader.java49
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoaderImpl.java73
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/setting/SettingsChangeNotifier.java46
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/setting/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java32
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/package-info.java23
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/BooleanTypeValidation.java42
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/FloatTypeValidation.java45
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java59
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/IntegerTypeValidation.java45
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/LongTypeValidation.java43
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java44
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringListTypeValidation.java41
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringTypeValidation.java40
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/TextTypeValidation.java40
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidation.java34
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidationModule.java39
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidations.java70
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/Validation.java33
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/package-info.java24
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/app/ProcessCommandWrapperImplTest.java158
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureProxyImplTest.java48
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureRule.java46
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/BadRequestExceptionTest.java85
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/MessageTest.java88
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/ServerExceptionTest.java40
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/health/TestStandaloneHealthChecker.java39
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java322
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java142
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java105
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java67
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java385
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestPluginA.java29
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestProjectUtils.java40
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java104
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java47
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterServlet.java43
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/edition/EditionBundledPluginsTest.java178
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java311
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/project/RekeyedProjectTest.java102
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java358
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java133
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/setting/ProjectConfigurationLoaderImplTest.java178
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/setting/SettingsChangeNotifierTest.java50
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/setting/TestProjectConfigurationLoader.java45
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/BooleanTypeValidationTest.java57
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/FloatTypeValidationTest.java56
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java76
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/IntegerTypeValidationTest.java63
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/LongTypeValidationTest.java62
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringListTypeValidationTest.java56
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringTypeValidationTest.java47
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/TextTypeValidationTest.java47
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationModuleTest.java34
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTest.java72
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTesting.java40
-rw-r--r--server/sonar-webserver-api/src/test/projects/.gitignore7
-rw-r--r--server/sonar-webserver-api/src/test/projects/README.txt3
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-report-plugin/pom.xml36
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/BasePlugin.java30
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java25
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-report-plugin/target/fake-report-plugin-0.1-SNAPSHOT.jarbin0 -> 3411 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/pom.xml36
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/BasePlugin.java30
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java25
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/target/fake-sqale-plugin-0.1-SNAPSHOT.jarbin0 -> 3407 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-views-plugin/pom.xml36
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/BasePlugin.java30
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java25
-rw-r--r--server/sonar-webserver-api/src/test/projects/fake-views-plugin/target/fake-views-plugin-0.1-SNAPSHOT.jarbin0 -> 3403 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/pom.xml22
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/pom.xml36
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/BasePlugin.java30
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/org/sonar/plugins/testbase/api/BaseApi.java25
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/target/test-base-plugin-0.2-SNAPSHOT.jarbin0 -> 3447 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin/pom.xml36
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin/src/BasePlugin.java30
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java25
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-base-plugin/target/test-base-plugin-0.1-SNAPSHOT.jarbin0 -> 3447 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-extend-plugin/pom.xml37
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-extend-plugin/src/ExtendPlugin.java30
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-extend-plugin/target/test-extend-plugin-0.1-SNAPSHOT.jarbin0 -> 2522 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-libs-plugin/pom.xml49
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-libs-plugin/src/LibsPlugin.java30
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-libs-plugin/target/test-libs-plugin-0.1-SNAPSHOT.jarbin0 -> 40139 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-require-plugin/pom.xml44
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-require-plugin/src/RequirePlugin.java35
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-require-plugin/target/test-require-plugin-0.1-SNAPSHOT.jarbin0 -> 2622 bytes
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/pom.xml44
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/src/RequirePlugin.java36
-rw-r--r--server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/target/test-requirenew-plugin-0.1-SNAPSHOT.jarbin0 -> 2689 bytes
-rw-r--r--server/sonar-webserver-api/src/test/resources/logback-test.xml26
149 files changed, 8882 insertions, 0 deletions
diff --git a/server/sonar-webserver-api/build.gradle b/server/sonar-webserver-api/build.gradle
new file mode 100644
index 00000000000..9c89a4b7de4
--- /dev/null
+++ b/server/sonar-webserver-api/build.gradle
@@ -0,0 +1,58 @@
+description = 'SonarQube WebServer internal APIs, used by other Web Server modules or Core Extensions'
+
+sonarqube {
+ properties {
+ property 'sonar.projectName', "${projectTitle} :: WebServer :: API"
+ }
+}
+
+sourceSets {
+ test {
+ resources {
+ srcDirs += ['src/test/projects']
+ }
+ }
+}
+
+configurations {
+ tests
+
+ testCompile.extendsFrom tests
+}
+
+dependencies {
+ // please keep the list grouped by configuration and ordered by name
+
+ compile 'com.google.guava:guava'
+ compile 'io.jsonwebtoken:jjwt-api'
+ compile 'io.jsonwebtoken:jjwt-impl'
+ compile project(':sonar-core')
+ compile project(':server:sonar-db-dao')
+ compile project(':server:sonar-process')
+ compile project(':server:sonar-server-common')
+ compile project(path: ':sonar-plugin-api', configuration: 'shadow')
+ compile project(':sonar-plugin-api-impl')
+ compile 'org.mindrot:jbcrypt'
+
+ compileOnly 'com.google.code.findbugs:jsr305'
+ compileOnly 'javax.servlet:javax.servlet-api'
+
+ testCompile 'org.assertj:assertj-guava'
+ testCompile 'com.google.code.findbugs:jsr305'
+ testCompile 'com.tngtech.java:junit-dataprovider'
+ testCompile 'javax.servlet:javax.servlet-api'
+ testCompile 'org.mockito:mockito-core'
+ testCompile project(':server:sonar-db-testing')
+ testCompile project(path: ":server:sonar-server-common", configuration: "tests")
+ testCompile project(path: ":server:sonar-webserver-ws", configuration: "tests")
+ testCompile project(':sonar-testing-harness')
+}
+
+task testJar(type: Jar) {
+ classifier = 'tests'
+ from sourceSets.test.output
+}
+
+artifacts {
+ tests testJar
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapper.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapper.java
new file mode 100644
index 00000000000..44bdc4377e3
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapper.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.app;
+
+public interface ProcessCommandWrapper {
+ /**
+ * Requests to the main process that SQ be restarted.
+ */
+ void requestSQRestart();
+
+ /**
+ * Requests to the main process that the WebServer is stopped.
+ */
+ void requestHardStop();
+
+ /**
+ * Notifies any listening process that the WebServer is operational.
+ */
+ void notifyOperational();
+
+ /**
+ * Checks whether the Compute Engine is operational.
+ */
+ boolean isCeOperational();
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapperImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapperImpl.java
new file mode 100644
index 00000000000..12b3f68543c
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/ProcessCommandWrapperImpl.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.app;
+
+import java.io.File;
+import org.sonar.api.config.Configuration;
+import org.sonar.process.ProcessId;
+import org.sonar.process.sharedmemoryfile.DefaultProcessCommands;
+import org.sonar.process.sharedmemoryfile.ProcessCommands;
+
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
+
+public class ProcessCommandWrapperImpl implements ProcessCommandWrapper {
+
+ private static final ProcessMethod<Void> SET_OPERATIONAL = processCommands -> {
+ processCommands.setOperational();
+ return null;
+ };
+ private static final ProcessMethod<Void> ASK_FOR_RESTART = processCommands -> {
+ processCommands.askForRestart();
+ return null;
+ };
+ private static final ProcessMethod<Void> ASK_FOR_HARD_STOP = processCommands -> {
+ processCommands.askForHardStop();
+ return null;
+ };
+ private static final ProcessMethod<Boolean> IS_OPERATIONAL = ProcessCommands::isOperational;
+
+ private final Configuration config;
+
+ public ProcessCommandWrapperImpl(Configuration config) {
+ this.config = config;
+ }
+
+ @Override
+ public void requestSQRestart() {
+ call(ASK_FOR_RESTART, selfProcessNumber());
+ }
+
+ @Override
+ public void requestHardStop() {
+ call(ASK_FOR_HARD_STOP, selfProcessNumber());
+ }
+
+ @Override
+ public void notifyOperational() {
+ call(SET_OPERATIONAL, selfProcessNumber());
+ }
+
+ @Override
+ public boolean isCeOperational() {
+ return call(IS_OPERATIONAL, ProcessId.COMPUTE_ENGINE.getIpcIndex());
+ }
+
+ private int selfProcessNumber() {
+ return nonNullAsInt(PROPERTY_PROCESS_INDEX);
+ }
+
+ private <T> T call(ProcessMethod<T> command, int processNumber) {
+ File shareDir = nonNullValueAsFile(PROPERTY_SHARED_PATH);
+ try (DefaultProcessCommands commands = DefaultProcessCommands.secondary(shareDir, processNumber)) {
+ return command.callOn(commands);
+ }
+ }
+
+ private interface ProcessMethod<T> {
+ T callOn(ProcessCommands processCommands);
+ }
+
+ private int nonNullAsInt(String key) {
+ return config.getInt(key).orElseThrow(() -> new IllegalArgumentException(String.format("Property %s is not set", key)));
+ }
+
+ private File nonNullValueAsFile(String key) {
+ return new File(config.get(key).orElseThrow(() -> new IllegalArgumentException(String.format("Property %s is not set", key))));
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolder.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolder.java
new file mode 100644
index 00000000000..dfc6c8c8414
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolder.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.app;
+
+/**
+ * Holds a boolean flag representing the restarting status of the WebServer.
+ * This boolean is {@code false} by default and can safely be changed concurrently using methods {@link #set()} and
+ * {@link #unset()}.
+ */
+public interface RestartFlagHolder {
+ /**
+ * @return whether restarting flag has been set or not.
+ */
+ boolean isRestarting();
+
+ /**
+ * Sets the restarting flag to {@code true}, no matter it already is or not.
+ */
+ void set();
+
+ /**
+ * Sets the restarting flag to {@code false}, no matter it already is or not.
+ */
+ void unset();
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolderImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolderImpl.java
new file mode 100644
index 00000000000..98354a8e77b
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/RestartFlagHolderImpl.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.app;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class RestartFlagHolderImpl implements RestartFlagHolder {
+ private final AtomicBoolean restarting = new AtomicBoolean(false);
+
+ @Override
+ public boolean isRestarting() {
+ return restarting.get();
+ }
+
+ @Override
+ public void set() {
+ restarting.set(true);
+ }
+
+ @Override
+ public void unset() {
+ restarting.set(false);
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/app/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/package-info.java
new file mode 100644
index 00000000000..7170de2498d
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/app/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.app;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeature.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeature.java
new file mode 100644
index 00000000000..7d60949c954
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeature.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.branch;
+
+interface BranchFeature {
+
+ boolean isEnabled();
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureExtension.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureExtension.java
new file mode 100644
index 00000000000..20fc40ff324
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureExtension.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.branch;
+
+import org.sonar.api.server.ServerSide;
+
+/**
+ * The branch plugin needs to implement this in order to know that the branch feature is supported
+ */
+@ServerSide
+public interface BranchFeatureExtension extends BranchFeature {
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxy.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxy.java
new file mode 100644
index 00000000000..8647d7c421f
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxy.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.branch;
+
+/**
+ * The goal of this class is to handle the 2 different use case :
+ * - The branch plugin exists, the proxy will redirect method calls to the plugin
+ * - No branch plugin, feature is disabled
+ */
+public interface BranchFeatureProxy extends BranchFeature {
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxyImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxyImpl.java
new file mode 100644
index 00000000000..634e59721a7
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/BranchFeatureProxyImpl.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.branch;
+
+public class BranchFeatureProxyImpl implements BranchFeatureProxy {
+
+ private final BranchFeatureExtension branchFeatureExtension;
+
+ public BranchFeatureProxyImpl() {
+ this.branchFeatureExtension = null;
+ }
+
+ public BranchFeatureProxyImpl(BranchFeatureExtension branchFeatureExtension) {
+ this.branchFeatureExtension = branchFeatureExtension;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return branchFeatureExtension != null && branchFeatureExtension.isEnabled();
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/package-info.java
new file mode 100644
index 00000000000..cb79dbcf31d
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/branch/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.branch;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/CeHttpClient.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/CeHttpClient.java
new file mode 100644
index 00000000000..f86a08efb62
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/CeHttpClient.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.ce.http;
+
+import java.util.Optional;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+public interface CeHttpClient {
+ Optional<ProtobufSystemInfo.SystemInfo> retrieveSystemInfo();
+
+ void changeLogLevel(LoggerLevel level);
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/package-info.java
new file mode 100644
index 00000000000..17b36c59125
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/ce/http/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.ce.http;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/BadRequestException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/BadRequestException.java
new file mode 100644
index 00000000000..3113d202d99
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/BadRequestException.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import com.google.common.base.MoreObjects;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.util.Arrays.asList;
+
+/**
+ * Request is not valid and can not be processed.
+ */
+public class BadRequestException extends ServerException {
+
+ private final transient List<String> errors;
+
+ private BadRequestException(List<String> errors) {
+ super(HTTP_BAD_REQUEST, errors.get(0));
+ this.errors = errors;
+ }
+
+ public static void checkRequest(boolean expression, String message, Object... messageArguments) {
+ if (!expression) {
+ throw create(format(message, messageArguments));
+ }
+ }
+
+ public static void checkRequest(boolean expression, List<String> messages) {
+ if (!expression) {
+ throw create(messages);
+ }
+ }
+
+ public static BadRequestException create(List<String> errorMessages) {
+ checkArgument(!errorMessages.isEmpty(), "At least one error message is required");
+ checkArgument(errorMessages.stream().noneMatch(message -> message == null || message.isEmpty()), "Message cannot be empty");
+ return new BadRequestException(errorMessages);
+ }
+
+ public static BadRequestException create(String... errorMessages) {
+ return create(asList(errorMessages));
+ }
+
+ public List<String> errors() {
+ return errors;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("errors", errors)
+ .toString();
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ForbiddenException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ForbiddenException.java
new file mode 100644
index 00000000000..d72eefbd02f
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ForbiddenException.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import com.google.common.base.Preconditions;
+
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+
+/**
+ * Permission denied. User does not have the required permissions.
+ */
+public class ForbiddenException extends ServerException {
+
+ public ForbiddenException(String message) {
+ super(HTTP_FORBIDDEN, Preconditions.checkNotNull(message));
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/Message.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/Message.java
new file mode 100644
index 00000000000..c069ead73d2
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/Message.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import com.google.common.base.Preconditions;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.String.format;
+
+public class Message {
+
+ private final String msg;
+
+ private Message(String format, Object... params) {
+ Preconditions.checkArgument(!isNullOrEmpty(format), "Message cannot be empty");
+ this.msg = format(format, params);
+ }
+
+ public String getMessage() {
+ return msg;
+ }
+
+ public static Message of(String msg, Object... arguments) {
+ return new Message(msg, arguments);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Message other = (Message) o;
+ return this.msg.equals(other.msg);
+ }
+
+ @Override
+ public int hashCode() {
+ return msg.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return msg;
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/NotFoundException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/NotFoundException.java
new file mode 100644
index 00000000000..f21a98b5157
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/NotFoundException.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import com.google.common.base.Optional;
+import javax.annotation.Nullable;
+
+import static java.lang.String.format;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+
+public class NotFoundException extends ServerException {
+
+ public NotFoundException(String message) {
+ super(HTTP_NOT_FOUND, message);
+ }
+
+ /**
+ * @throws NotFoundException if the value if null
+ * @return the value
+ */
+ public static <T> T checkFound(@Nullable T value, String message, Object... messageArguments) {
+ if (value == null) {
+ throw new NotFoundException(format(message, messageArguments));
+ }
+
+ return value;
+ }
+
+ /**
+ * @throws NotFoundException if the value is not present
+ * @return the value
+ */
+ public static <T> T checkFoundWithOptional(Optional<T> value, String message, Object... messageArguments) {
+ if (!value.isPresent()) {
+ throw new NotFoundException(format(message, messageArguments));
+ }
+
+ return value.get();
+ }
+
+ public static <T> T checkFoundWithOptional(java.util.Optional<T> value, String message, Object... messageArguments) {
+ if (!value.isPresent()) {
+ throw new NotFoundException(format(message, messageArguments));
+ }
+
+ return value.get();
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ServerException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ServerException.java
new file mode 100644
index 00000000000..491c17ed437
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ServerException.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import static java.util.Objects.requireNonNull;
+
+public class ServerException extends RuntimeException {
+ private final int httpCode;
+
+ public ServerException(int httpCode, String message) {
+ super(requireNonNull(message, "Error message cannot be null"));
+ this.httpCode = httpCode;
+ }
+
+ public int httpCode() {
+ return httpCode;
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java
new file mode 100644
index 00000000000..0b4af12beee
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+
+/**
+ * User needs to be authenticated. HTTP request is generally redirected to login form.
+ */
+public class UnauthorizedException extends ServerException {
+
+ public UnauthorizedException(String message) {
+ super(HTTP_UNAUTHORIZED, message);
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/package-info.java
new file mode 100644
index 00000000000..c1ac144f25a
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/health/ClusterHealth.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/ClusterHealth.java
new file mode 100644
index 00000000000..23fcfca9f7a
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/ClusterHealth.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.health;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import org.sonar.process.cluster.health.NodeHealth;
+
+import static com.google.common.collect.ImmutableSet.copyOf;
+import static java.util.Objects.requireNonNull;
+
+public class ClusterHealth {
+ private final Health health;
+ private final Set<NodeHealth> nodes;
+
+ public ClusterHealth(Health health, Set<NodeHealth> nodes) {
+ this.health = requireNonNull(health, "health can't be null");
+ this.nodes = copyOf(requireNonNull(nodes, "nodes can't be null"));
+ }
+
+ public Health getHealth() {
+ return health;
+ }
+
+ public Set<NodeHealth> getNodes() {
+ return nodes;
+ }
+
+ public Optional<NodeHealth> getNodeHealth(String nodeName) {
+ return nodes.stream()
+ .filter(node -> nodeName.equals(node.getDetails().getName()))
+ .findFirst();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ClusterHealth that = (ClusterHealth) o;
+ return Objects.equals(health, that.health) &&
+ Objects.equals(nodes, that.nodes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(health, nodes);
+ }
+
+ @Override
+ public String toString() {
+ return "ClusterHealth{" +
+ "health=" + health +
+ ", nodes=" + nodes +
+ '}';
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/health/Health.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/Health.java
new file mode 100644
index 00000000000..d5b49449bbe
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/Health.java
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.health;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+public class Health {
+ /**
+ * The GREEN status without any cause as a constant, for convenience and optimisation.
+ */
+ public static final Health GREEN = newHealthCheckBuilder()
+ .setStatus(Status.GREEN)
+ .build();
+
+ private final Status status;
+ private final Set<String> causes;
+
+ public Health(Builder builder) {
+ this.status = builder.status;
+ this.causes = ImmutableSet.copyOf(builder.causes);
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public Set<String> getCauses() {
+ return causes;
+ }
+
+ public static Builder newHealthCheckBuilder() {
+ return new Builder();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Health health = (Health) o;
+ return status == health.status &&
+ Objects.equals(causes, health.causes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(status, causes);
+ }
+
+ @Override
+ public String toString() {
+ return "Health{" + status +
+ ", causes=" + causes +
+ '}';
+ }
+
+ /**
+ * Builder of {@link Health} which supports being reused for optimization.
+ */
+ public static class Builder {
+ private Status status;
+ private Set<String> causes = new HashSet<>(0);
+
+ private Builder() {
+ // use static factory method
+ }
+
+ public Builder clear() {
+ this.status = null;
+ this.causes.clear();
+ return this;
+ }
+
+ public Builder setStatus(Status status) {
+ this.status = checkStatus(status);
+ return this;
+ }
+
+ public Builder addCause(String cause) {
+ requireNonNull(cause, "cause can't be null");
+ checkArgument(!cause.trim().isEmpty(), "cause can't be empty");
+ causes.add(cause);
+ return this;
+ }
+
+ public Health build() {
+ checkStatus(this.status);
+ return new Health(this);
+ }
+
+ private static Status checkStatus(Status status) {
+ return requireNonNull(status, "status can't be null");
+ }
+ }
+
+ public enum Status {
+ /**
+ * Fully working
+ */
+ GREEN,
+ /**
+ * Yellow: Working but something must be fixed to make SQ fully operational
+ */
+ YELLOW,
+ /**
+ * Red: Not working
+ */
+ RED
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/health/HealthChecker.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/HealthChecker.java
new file mode 100644
index 00000000000..80fb8aed1bf
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/HealthChecker.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.health;
+
+public interface HealthChecker {
+ /**
+ * Perform a check of the health of the current SonarQube node, either as a standalone node or as a member
+ * of a cluster.
+ */
+ Health checkNode();
+
+ /**
+ * Perform a check of the health of the SonarQube cluster.
+ *
+ * @throws IllegalStateException if clustering is not enabled.
+ */
+ ClusterHealth checkCluster();
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/health/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/package-info.java
new file mode 100644
index 00000000000..18ea762c56b
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/health/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.health;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/ClusterFeature.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/ClusterFeature.java
new file mode 100644
index 00000000000..24d8617a4e1
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/ClusterFeature.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.ExtensionPoint;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+@ExtensionPoint
+public interface ClusterFeature {
+
+ boolean isEnabled();
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/Platform.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/Platform.java
new file mode 100644
index 00000000000..22f3e451a97
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/Platform.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.core.platform.ComponentContainer;
+
+public interface Platform {
+ void doStart();
+
+ Status status();
+
+ ComponentContainer getContainer();
+
+ enum Status {
+ BOOTING, SAFEMODE, STARTING, UP
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/SystemInfoWriter.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/SystemInfoWriter.java
new file mode 100644
index 00000000000..3a82f471f6a
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/SystemInfoWriter.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.utils.text.JsonWriter;
+
+public interface SystemInfoWriter {
+ void write(JsonWriter json) throws Exception;
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/package-info.java
new file mode 100644
index 00000000000..71d187381a7
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/platform/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.platform;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java
new file mode 100644
index 00000000000..0fffb7b7d78
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/InstalledPlugin.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FileUtils;
+import org.sonar.core.platform.PluginInfo;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class InstalledPlugin {
+ private final PluginInfo plugin;
+ private final FileAndMd5 loadedJar;
+ @Nullable
+ private final FileAndMd5 compressedJar;
+
+ public InstalledPlugin(PluginInfo plugin, FileAndMd5 loadedJar, @Nullable FileAndMd5 compressedJar) {
+ this.plugin = requireNonNull(plugin);
+ this.loadedJar = requireNonNull(loadedJar);
+ this.compressedJar = compressedJar;
+ }
+
+ public PluginInfo getPluginInfo() {
+ return plugin;
+ }
+
+ public FileAndMd5 getLoadedJar() {
+ return loadedJar;
+ }
+
+ @Nullable
+ public FileAndMd5 getCompressedJar() {
+ return compressedJar;
+ }
+
+ @Immutable
+ public static final class FileAndMd5 {
+ private final File file;
+ private final String md5;
+
+ public FileAndMd5(File file) {
+ try (InputStream fis = FileUtils.openInputStream(file)) {
+ this.file = file;
+ this.md5 = DigestUtils.md5Hex(fis);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to compute md5 of " + file, e);
+ }
+ }
+
+ public File getFile() {
+ return file;
+ }
+
+ public String getMd5() {
+ return md5;
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java
new file mode 100644
index 00000000000..d61efac304c
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginDownloader.java
@@ -0,0 +1,165 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.base.Optional;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.picocontainer.Startable;
+import org.sonar.api.utils.HttpDownloader;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.updatecenter.common.Release;
+import org.sonar.updatecenter.common.UpdateCenter;
+import org.sonar.updatecenter.common.Version;
+
+import static org.apache.commons.io.FileUtils.copyFile;
+import static org.apache.commons.io.FileUtils.copyFileToDirectory;
+import static org.apache.commons.io.FileUtils.forceMkdir;
+import static org.apache.commons.io.FileUtils.toFile;
+import static org.apache.commons.lang.StringUtils.substringAfterLast;
+import static org.sonar.core.util.FileUtils.deleteQuietly;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+/**
+ * Downloads plugins from update center. Files are copied in the directory extensions/downloads and then
+ * moved to extensions/plugins after server restart.
+ */
+public class PluginDownloader implements Startable {
+
+ private static final Logger LOG = Loggers.get(PluginDownloader.class);
+ private static final String TMP_SUFFIX = "tmp";
+ private static final String PLUGIN_EXTENSION = "jar";
+
+ private final UpdateCenterMatrixFactory updateCenterMatrixFactory;
+ private final HttpDownloader downloader;
+ private final File downloadDir;
+
+ public PluginDownloader(UpdateCenterMatrixFactory updateCenterMatrixFactory, HttpDownloader downloader,
+ ServerFileSystem fileSystem) {
+ this.updateCenterMatrixFactory = updateCenterMatrixFactory;
+ this.downloader = downloader;
+ this.downloadDir = fileSystem.getDownloadedPluginsDir();
+ }
+
+ /**
+ * Deletes the temporary files remaining from previous downloads
+ */
+ @Override
+ public void start() {
+ try {
+ forceMkdir(downloadDir);
+ for (File tempFile : listTempFile(this.downloadDir)) {
+ deleteQuietly(tempFile);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create the directory: " + downloadDir, e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+
+ public void cancelDownloads() {
+ try {
+ if (downloadDir.exists()) {
+ org.sonar.core.util.FileUtils.cleanDirectory(downloadDir);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to clean the plugin downloads directory: " + downloadDir, e);
+ }
+ }
+
+ public List<String> getDownloadedPluginFilenames() {
+ List<String> names = new ArrayList<>();
+ for (File file : listPlugins(this.downloadDir)) {
+ names.add(file.getName());
+ }
+ return names;
+ }
+
+ /**
+ * @return the list of download plugins as {@link PluginInfo} instances
+ */
+ public Collection<PluginInfo> getDownloadedPlugins() {
+ return listPlugins(this.downloadDir)
+ .stream()
+ .map(PluginInfo::create)
+ .collect(MoreCollectors.toList());
+ }
+
+ public void download(String pluginKey, Version version) {
+ Optional<UpdateCenter> updateCenter = updateCenterMatrixFactory.getUpdateCenter(true);
+ if (updateCenter.isPresent()) {
+ List<Release> installablePlugins = updateCenter.get().findInstallablePlugins(pluginKey, version);
+ checkRequest(!installablePlugins.isEmpty(), "Error while downloading plugin '%s' with version '%s'. No compatible plugin found.", pluginKey, version.getName());
+ for (Release release : installablePlugins) {
+ try {
+ downloadRelease(release);
+ } catch (Exception e) {
+ String message = String.format("Fail to download the plugin (%s, version %s) from %s (error is : %s)",
+ release.getArtifact().getKey(), release.getVersion().getName(), release.getDownloadUrl(), e.getMessage());
+ LOG.debug(message, e);
+ throw new IllegalStateException(message, e);
+ }
+ }
+ }
+ }
+
+ private void downloadRelease(Release release) throws URISyntaxException, IOException {
+ String url = release.getDownloadUrl();
+
+ URI uri = new URI(url);
+ if (url.startsWith("file:")) {
+ // used for tests
+ File file = toFile(uri.toURL());
+ copyFileToDirectory(file, downloadDir);
+ } else {
+ String filename = substringAfterLast(uri.getPath(), "/");
+ if (!filename.endsWith("." + PLUGIN_EXTENSION)) {
+ filename = release.getKey() + "-" + release.getVersion() + "." + PLUGIN_EXTENSION;
+ }
+ File targetFile = new File(downloadDir, filename);
+ File tempFile = new File(downloadDir, filename + "." + TMP_SUFFIX);
+ downloader.download(uri, tempFile);
+ copyFile(tempFile, targetFile);
+ deleteQuietly(tempFile);
+ }
+ }
+
+ private static Collection<File> listTempFile(File dir) {
+ return FileUtils.listFiles(dir, new String[] {TMP_SUFFIX}, false);
+ }
+
+ private static Collection<File> listPlugins(File dir) {
+ return FileUtils.listFiles(dir, new String[] {PLUGIN_EXTENSION}, false);
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java
new file mode 100644
index 00000000000..e65336a7586
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginFileSystem.java
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.jar.JarInputStream;
+import java.util.jar.Pack200;
+import java.util.zip.GZIPOutputStream;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.server.plugins.InstalledPlugin.FileAndMd5;
+
+import static com.google.common.base.Preconditions.checkState;
+
+@ServerSide
+public class PluginFileSystem {
+
+ public static final String PROPERTY_PLUGIN_COMPRESSION_ENABLE = "sonar.pluginsCompression.enable";
+ private static final Logger LOG = Loggers.get(PluginFileSystem.class);
+
+ private final Configuration configuration;
+ private final Map<String, InstalledPlugin> installedFiles = new HashMap<>();
+
+ public PluginFileSystem(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ /**
+ * @param plugin
+ * @param loadedJar the JAR loaded by classloaders. It differs from {@code plugin.getJarFile()}
+ * which is the initial location of JAR as seen by users
+ */
+ public void addInstalledPlugin(PluginInfo plugin, File loadedJar) {
+ checkState(!installedFiles.containsKey(plugin.getKey()), "Plugin %s is already loaded", plugin.getKey());
+ checkState(loadedJar.exists(), "loadedJar does not exist: %s", loadedJar);
+
+ Optional<File> compressed = compressJar(plugin, loadedJar);
+ InstalledPlugin installedFile = new InstalledPlugin(
+ plugin,
+ new FileAndMd5(loadedJar),
+ compressed.map(FileAndMd5::new).orElse(null));
+ installedFiles.put(plugin.getKey(), installedFile);
+ }
+
+ public Optional<InstalledPlugin> getInstalledPlugin(String pluginKey) {
+ return Optional.ofNullable(installedFiles.get(pluginKey));
+ }
+
+ public Collection<InstalledPlugin> getInstalledFiles() {
+ return installedFiles.values();
+ }
+
+ private Optional<File> compressJar(PluginInfo plugin, File jar) {
+ if (!configuration.getBoolean(PROPERTY_PLUGIN_COMPRESSION_ENABLE).orElse(false)) {
+ return Optional.empty();
+ }
+
+ Path targetPack200 = getPack200Path(jar.toPath());
+ Path sourcePack200Path = getPack200Path(plugin.getNonNullJarFile().toPath());
+
+ // check if packed file was deployed alongside the jar. If that's the case, use it instead of generating it (SONAR-10395).
+ if (sourcePack200Path.toFile().exists()) {
+ try {
+ LOG.debug("Found pack200: " + sourcePack200Path);
+ Files.copy(sourcePack200Path, targetPack200);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to copy pack200 file from " + sourcePack200Path + " to " + targetPack200, e);
+ }
+ } else {
+ pack200(jar.toPath(), targetPack200, plugin.getKey());
+ }
+ return Optional.of(targetPack200.toFile());
+ }
+
+ private static void pack200(Path jarPath, Path toPack200Path, String pluginKey) {
+ Profiler profiler = Profiler.create(LOG);
+ profiler.startInfo("Compressing plugin " + pluginKey + " [pack200]");
+
+ try (JarInputStream in = new JarInputStream(new BufferedInputStream(Files.newInputStream(jarPath)));
+ OutputStream out = new GZIPOutputStream(new BufferedOutputStream(Files.newOutputStream(toPack200Path)))) {
+ Pack200.newPacker().pack(in, out);
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Fail to pack200 plugin [%s] '%s' to '%s'", pluginKey, jarPath, toPack200Path), e);
+ }
+ profiler.stopInfo();
+ }
+
+ private static Path getPack200Path(Path jar) {
+ String jarFileName = jar.getFileName().toString();
+ String filename = jarFileName.substring(0, jarFileName.length() - 3) + "pack.gz";
+ return jar.resolveSibling(filename);
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java
new file mode 100644
index 00000000000..8253dbaf8c1
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginUninstaller.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.commons.io.FileUtils;
+import org.picocontainer.Startable;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.platform.ServerFileSystem;
+
+import static java.lang.String.format;
+import static org.apache.commons.io.FileUtils.forceMkdir;
+
+public class PluginUninstaller implements Startable {
+ private static final String PLUGIN_EXTENSION = "jar";
+ private final ServerPluginRepository serverPluginRepository;
+ private final File uninstallDir;
+
+ public PluginUninstaller(ServerPluginRepository serverPluginRepository, ServerFileSystem fs) {
+ this.serverPluginRepository = serverPluginRepository;
+ this.uninstallDir = fs.getUninstalledPluginsDir();
+ }
+
+ private static Collection<File> listJarFiles(File dir) {
+ if (dir.exists()) {
+ return FileUtils.listFiles(dir, new String[] {PLUGIN_EXTENSION}, false);
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void start() {
+ try {
+ forceMkdir(uninstallDir);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create the directory: " + uninstallDir, e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+
+ public void uninstall(String pluginKey) {
+ ensurePluginIsInstalled(pluginKey);
+ serverPluginRepository.uninstall(pluginKey, uninstallDir);
+ }
+
+ public void cancelUninstalls() {
+ serverPluginRepository.cancelUninstalls(uninstallDir);
+ }
+
+ /**
+ * @return the list of plugins to be uninstalled as {@link PluginInfo} instances
+ */
+ public Collection<PluginInfo> getUninstalledPlugins() {
+ return listJarFiles(uninstallDir)
+ .stream()
+ .map(PluginInfo::create)
+ .collect(MoreCollectors.toList());
+ }
+
+ private void ensurePluginIsInstalled(String key) {
+ if (!serverPluginRepository.hasPlugin(key)) {
+ throw new IllegalArgumentException(format("Plugin [%s] is not installed", key));
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java
new file mode 100644
index 00000000000..1970d4475a5
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.io.File;
+import org.apache.commons.io.FileUtils;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.ZipUtils;
+import org.sonar.core.platform.ExplodedPlugin;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginJarExploder;
+import org.sonar.server.platform.ServerFileSystem;
+
+import static org.apache.commons.io.FileUtils.forceMkdir;
+
+@ServerSide
+public class ServerPluginJarExploder extends PluginJarExploder {
+ private final ServerFileSystem fs;
+ private final PluginFileSystem pluginFileSystem;
+
+ public ServerPluginJarExploder(ServerFileSystem fs, PluginFileSystem pluginFileSystem) {
+ this.fs = fs;
+ this.pluginFileSystem = pluginFileSystem;
+ }
+
+ /**
+ * JAR files of directory extensions/plugins can be moved when server is up and plugins are uninstalled.
+ * For this reason these files must not be locked by classloaders. They are copied to the directory
+ * web/deploy/plugins in order to be loaded by {@link org.sonar.core.platform.PluginLoader}.
+ */
+ @Override
+ public ExplodedPlugin explode(PluginInfo pluginInfo) {
+ File toDir = new File(fs.getDeployedPluginsDir(), pluginInfo.getKey());
+ try {
+ forceMkdir(toDir);
+ org.sonar.core.util.FileUtils.cleanDirectory(toDir);
+
+ File jarSource = pluginInfo.getNonNullJarFile();
+ File jarTarget = new File(toDir, jarSource.getName());
+
+ FileUtils.copyFile(jarSource, jarTarget);
+ ZipUtils.unzip(jarSource, toDir, newLibFilter());
+ ExplodedPlugin explodedPlugin = explodeFromUnzippedDir(pluginInfo.getKey(), jarTarget, toDir);
+ pluginFileSystem.addInstalledPlugin(pluginInfo, jarTarget);
+ return explodedPlugin;
+ } catch (Exception e) {
+ throw new IllegalStateException(String.format(
+ "Fail to unzip plugin [%s] %s to %s", pluginInfo.getKey(), pluginInfo.getNonNullJarFile().getAbsolutePath(), toDir.getAbsolutePath()), e);
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java
new file mode 100644
index 00000000000..ecc3abc2d76
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java
@@ -0,0 +1,372 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.CheckForNull;
+import org.apache.commons.io.FileUtils;
+import org.picocontainer.Startable;
+import org.sonar.api.Plugin;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginLoader;
+import org.sonar.core.platform.PluginRepository;
+import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.updatecenter.common.Version;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static org.apache.commons.io.FileUtils.moveFile;
+import static org.apache.commons.io.FileUtils.moveFileToDirectory;
+import static org.sonar.core.util.FileUtils.deleteQuietly;
+
+/**
+ * Entry point to install and load plugins on server startup. It manages
+ * <ul>
+ * <li>installation of new plugins (effective after server startup)</li>
+ * <li>un-installation of plugins (effective after server startup)</li>
+ * <li>cancel pending installations/un-installations</li>
+ * <li>instantiation of plugin entry-points</li>
+ * </ul>
+ */
+public class ServerPluginRepository implements PluginRepository, Startable {
+
+ private static final Logger LOG = Loggers.get(ServerPluginRepository.class);
+ private static final String[] JAR_FILE_EXTENSIONS = new String[] {"jar"};
+ // List of plugins that are silently removed if installed
+ private static final Set<String> DEFAULT_BLACKLISTED_PLUGINS = ImmutableSet.of("scmactivity", "issuesreport", "genericcoverage");
+ // List of plugins that should prevent the server to finish its startup
+ private static final Set<String> FORBIDDEN_COMPATIBLE_PLUGINS = ImmutableSet.of("sqale", "report", "views");
+ private static final Joiner SLASH_JOINER = Joiner.on(" / ").skipNulls();
+ private static final String NOT_STARTED_YET = "not started yet";
+
+ private final SonarRuntime runtime;
+ private final ServerFileSystem fs;
+ private final PluginLoader loader;
+ private final AtomicBoolean started = new AtomicBoolean(false);
+ private Set<String> blacklistedPluginKeys = DEFAULT_BLACKLISTED_PLUGINS;
+
+ // following fields are available after startup
+ private final Map<String, PluginInfo> pluginInfosByKeys = new HashMap<>();
+ private final Map<String, Plugin> pluginInstancesByKeys = new HashMap<>();
+ private final Map<ClassLoader, String> keysByClassLoader = new HashMap<>();
+
+ public ServerPluginRepository(SonarRuntime runtime, ServerFileSystem fs, PluginLoader loader) {
+ this.runtime = runtime;
+ this.fs = fs;
+ this.loader = loader;
+ }
+
+ @VisibleForTesting
+ void setBlacklistedPluginKeys(Set<String> keys) {
+ this.blacklistedPluginKeys = keys;
+ }
+
+ @Override
+ public void start() {
+ loadPreInstalledPlugins();
+ moveDownloadedPlugins();
+ unloadIncompatiblePlugins();
+ logInstalledPlugins();
+ loadInstances();
+ started.set(true);
+ }
+
+ @Override
+ public void stop() {
+ // close classloaders
+ loader.unload(pluginInstancesByKeys.values());
+ pluginInstancesByKeys.clear();
+ pluginInfosByKeys.clear();
+ keysByClassLoader.clear();
+ started.set(true);
+ }
+
+ /**
+ * Return the key of the plugin the extension (in the sense of {@link Plugin.Context#addExtension(Object)} is coming from.
+ */
+ @CheckForNull
+ public String getPluginKey(Object extension) {
+ return keysByClassLoader.get(extension.getClass().getClassLoader());
+ }
+
+ /**
+ * Load the plugins that are located in extensions/plugins. Blacklisted plugins are
+ * deleted.
+ */
+ private void loadPreInstalledPlugins() {
+ for (File file : listJarFiles(fs.getInstalledPluginsDir())) {
+ PluginInfo info = PluginInfo.create(file);
+ registerPluginInfo(info);
+ }
+ }
+
+ /**
+ * Move the plugins recently downloaded to extensions/plugins.
+ */
+ private void moveDownloadedPlugins() {
+ if (fs.getDownloadedPluginsDir().exists()) {
+ for (File sourceFile : listJarFiles(fs.getDownloadedPluginsDir())) {
+ overrideAndRegisterPlugin(sourceFile);
+ }
+ }
+ }
+
+ private void registerPluginInfo(PluginInfo info) {
+ String pluginKey = info.getKey();
+ if (blacklistedPluginKeys.contains(pluginKey)) {
+ LOG.warn("Plugin {} [{}] is blacklisted and is being uninstalled", info.getName(), pluginKey);
+ deleteQuietly(info.getNonNullJarFile());
+ return;
+ }
+ if (FORBIDDEN_COMPATIBLE_PLUGINS.contains(pluginKey)) {
+ throw MessageException.of(String.format("Plugin '%s' is no longer compatible with this version of SonarQube", pluginKey));
+ }
+ PluginInfo existing = pluginInfosByKeys.put(pluginKey, info);
+ if (existing != null) {
+ throw MessageException.of(format("Found two versions of the plugin %s [%s] in the directory extensions/plugins. Please remove one of %s or %s.",
+ info.getName(), pluginKey, info.getNonNullJarFile().getName(), existing.getNonNullJarFile().getName()));
+ }
+
+ }
+
+ /**
+ * Move or copy plugin to directory extensions/plugins. If a version of this plugin
+ * already exists then it's deleted.
+ */
+ private void overrideAndRegisterPlugin(File sourceFile) {
+ File destDir = fs.getInstalledPluginsDir();
+ File destFile = new File(destDir, sourceFile.getName());
+ if (destFile.exists()) {
+ // plugin with same filename already installed
+ deleteQuietly(destFile);
+ }
+
+ try {
+ moveFile(sourceFile, destFile);
+
+ } catch (IOException e) {
+ throw new IllegalStateException(format("Fail to move plugin: %s to %s",
+ sourceFile.getAbsolutePath(), destFile.getAbsolutePath()), e);
+ }
+
+ PluginInfo info = PluginInfo.create(destFile);
+ PluginInfo existing = pluginInfosByKeys.put(info.getKey(), info);
+ if (existing != null) {
+ if (!existing.getNonNullJarFile().getName().equals(destFile.getName())) {
+ deleteQuietly(existing.getNonNullJarFile());
+ }
+ LOG.info("Plugin {} [{}] updated to version {}", info.getName(), info.getKey(), info.getVersion());
+ } else {
+ LOG.info("Plugin {} [{}] installed", info.getName(), info.getKey());
+ }
+ }
+
+ /**
+ * Removes the plugins that are not compatible with current environment.
+ */
+ private void unloadIncompatiblePlugins() {
+ // loop as long as the previous loop ignored some plugins. That allows to support dependencies
+ // on many levels, for example D extends C, which extends B, which requires A. If A is not installed,
+ // then B, C and D must be ignored. That's not possible to achieve this algorithm with a single
+ // iteration over plugins.
+ Set<String> removedKeys = new HashSet<>();
+ do {
+ removedKeys.clear();
+ for (PluginInfo plugin : pluginInfosByKeys.values()) {
+ if (!isCompatible(plugin, runtime, pluginInfosByKeys)) {
+ removedKeys.add(plugin.getKey());
+ }
+ }
+ for (String removedKey : removedKeys) {
+ pluginInfosByKeys.remove(removedKey);
+ }
+ } while (!removedKeys.isEmpty());
+ }
+
+ @VisibleForTesting
+ static boolean isCompatible(PluginInfo plugin, SonarRuntime runtime, Map<String, PluginInfo> allPluginsByKeys) {
+ if (Strings.isNullOrEmpty(plugin.getMainClass()) && Strings.isNullOrEmpty(plugin.getBasePlugin())) {
+ LOG.warn("Plugin {} [{}] is ignored because entry point class is not defined", plugin.getName(), plugin.getKey());
+ return false;
+ }
+
+ if (!plugin.isCompatibleWith(runtime.getApiVersion().toString())) {
+ throw MessageException.of(format(
+ "Plugin %s [%s] requires at least SonarQube %s", plugin.getName(), plugin.getKey(), plugin.getMinimalSqVersion()));
+ }
+
+ if (!Strings.isNullOrEmpty(plugin.getBasePlugin()) && !allPluginsByKeys.containsKey(plugin.getBasePlugin())) {
+ // it extends a plugin that is not installed
+ LOG.warn("Plugin {} [{}] is ignored because its base plugin [{}] is not installed", plugin.getName(), plugin.getKey(), plugin.getBasePlugin());
+ return false;
+ }
+
+ for (PluginInfo.RequiredPlugin requiredPlugin : plugin.getRequiredPlugins()) {
+ PluginInfo installedRequirement = allPluginsByKeys.get(requiredPlugin.getKey());
+ if (installedRequirement == null) {
+ // it requires a plugin that is not installed
+ LOG.warn("Plugin {} [{}] is ignored because the required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), requiredPlugin.getKey());
+ return false;
+ }
+ Version installedRequirementVersion = installedRequirement.getVersion();
+ if (installedRequirementVersion != null && requiredPlugin.getMinimalVersion().compareToIgnoreQualifier(installedRequirementVersion) > 0) {
+ // it requires a more recent version
+ LOG.warn("Plugin {} [{}] is ignored because the version {} of required plugin [{}] is not supported", plugin.getName(), plugin.getKey(),
+ requiredPlugin.getKey(), requiredPlugin.getMinimalVersion());
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void logInstalledPlugins() {
+ List<PluginInfo> orderedPlugins = Ordering.natural().sortedCopy(pluginInfosByKeys.values());
+ for (PluginInfo plugin : orderedPlugins) {
+ LOG.info("Deploy plugin {}", SLASH_JOINER.join(plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild()));
+ }
+ }
+
+ private void loadInstances() {
+ pluginInstancesByKeys.putAll(loader.load(pluginInfosByKeys));
+
+ for (Map.Entry<String, Plugin> e : pluginInstancesByKeys.entrySet()) {
+ keysByClassLoader.put(e.getValue().getClass().getClassLoader(), e.getKey());
+ }
+ }
+
+ /**
+ * Uninstall a plugin and its dependents
+ */
+ public void uninstall(String pluginKey, File uninstallDir) {
+ Set<String> uninstallKeys = new HashSet<>();
+ uninstallKeys.add(pluginKey);
+ appendDependentPluginKeys(pluginKey, uninstallKeys);
+
+ for (String uninstallKey : uninstallKeys) {
+ PluginInfo info = getPluginInfo(uninstallKey);
+
+ try {
+ if (!getPluginFile(info).exists()) {
+ LOG.info("Plugin already uninstalled: {} [{}]", info.getName(), info.getKey());
+ continue;
+ }
+
+ LOG.info("Uninstalling plugin {} [{}]", info.getName(), info.getKey());
+
+ File masterFile = getPluginFile(info);
+ moveFileToDirectory(masterFile, uninstallDir, true);
+ } catch (IOException e) {
+ throw new IllegalStateException(format("Fail to uninstall plugin %s [%s]", info.getName(), info.getKey()), e);
+ }
+ }
+ }
+
+ public void cancelUninstalls(File uninstallDir) {
+ for (File file : listJarFiles(uninstallDir)) {
+ try {
+ moveFileToDirectory(file, fs.getInstalledPluginsDir(), false);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to cancel plugin uninstalls", e);
+ }
+ }
+ }
+
+ /**
+ * Appends dependent plugins, only the ones that still exist in the plugins folder.
+ */
+ private void appendDependentPluginKeys(String pluginKey, Set<String> appendTo) {
+ for (PluginInfo otherPlugin : getPluginInfos()) {
+ if (!otherPlugin.getKey().equals(pluginKey)) {
+ for (PluginInfo.RequiredPlugin requirement : otherPlugin.getRequiredPlugins()) {
+ if (requirement.getKey().equals(pluginKey)) {
+ appendTo.add(otherPlugin.getKey());
+ appendDependentPluginKeys(otherPlugin.getKey(), appendTo);
+ }
+ }
+ }
+ }
+ }
+
+ private File getPluginFile(PluginInfo info) {
+ // we don't reuse info.getFile() just to be sure that file is located in from extensions/plugins
+ return new File(fs.getInstalledPluginsDir(), info.getNonNullJarFile().getName());
+ }
+
+ public Map<String, PluginInfo> getPluginInfosByKeys() {
+ return pluginInfosByKeys;
+ }
+
+ @Override
+ public Collection<PluginInfo> getPluginInfos() {
+ checkState(started.get(), NOT_STARTED_YET);
+ return ImmutableList.copyOf(pluginInfosByKeys.values());
+ }
+
+ @Override
+ public PluginInfo getPluginInfo(String key) {
+ checkState(started.get(), NOT_STARTED_YET);
+ PluginInfo info = pluginInfosByKeys.get(key);
+ if (info == null) {
+ throw new IllegalArgumentException(format("Plugin [%s] does not exist", key));
+ }
+ return info;
+ }
+
+ @Override
+ public Plugin getPluginInstance(String key) {
+ checkState(started.get(), NOT_STARTED_YET);
+ Plugin plugin = pluginInstancesByKeys.get(key);
+ checkArgument(plugin != null, "Plugin [%s] does not exist", key);
+ return plugin;
+ }
+
+ @Override
+ public boolean hasPlugin(String key) {
+ checkState(started.get(), NOT_STARTED_YET);
+ return pluginInfosByKeys.containsKey(key);
+ }
+
+ private static Collection<File> listJarFiles(File dir) {
+ if (dir.exists()) {
+ return FileUtils.listFiles(dir, JAR_FILE_EXTENSIONS, false);
+ }
+ return Collections.emptyList();
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java
new file mode 100644
index 00000000000..2f5d0b2d357
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterClient.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.base.Optional;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.UriReader;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.process.ProcessProperties;
+import org.sonar.updatecenter.common.UpdateCenter;
+import org.sonar.updatecenter.common.UpdateCenterDeserializer;
+import org.sonar.updatecenter.common.UpdateCenterDeserializer.Mode;
+
+/**
+ * HTTP client to load data from the remote update center hosted at https://update.sonarsource.org.
+ *
+ * @since 2.4
+ */
+@Properties({
+ @Property(
+ key = UpdateCenterClient.URL_PROPERTY,
+ defaultValue = "https://update.sonarsource.org/update-center.properties",
+ name = "Update Center URL",
+ category = "Update Center",
+ project = false,
+ // hidden from UI
+ global = false)
+})
+public class UpdateCenterClient {
+
+ public static final String URL_PROPERTY = "sonar.updatecenter.url";
+ public static final int PERIOD_IN_MILLISECONDS = 60 * 60 * 1000;
+
+ private final URI uri;
+ private final UriReader uriReader;
+ private final boolean isActivated;
+ private UpdateCenter pluginCenter = null;
+ private long lastRefreshDate = 0;
+
+ public UpdateCenterClient(UriReader uriReader, Configuration config) throws URISyntaxException {
+ this.uriReader = uriReader;
+ this.uri = new URI(config.get(URL_PROPERTY).get());
+ this.isActivated = config.getBoolean(ProcessProperties.Property.SONAR_UPDATECENTER_ACTIVATE.getKey()).get();
+ Loggers.get(getClass()).info("Update center: " + uriReader.description(uri));
+ }
+
+ public Optional<UpdateCenter> getUpdateCenter() {
+ return getUpdateCenter(false);
+ }
+
+ public Optional<UpdateCenter> getUpdateCenter(boolean forceRefresh) {
+ if (!isActivated) {
+ return Optional.absent();
+ }
+
+ if (pluginCenter == null || forceRefresh || needsRefresh()) {
+ pluginCenter = init();
+ lastRefreshDate = System.currentTimeMillis();
+ }
+ return Optional.fromNullable(pluginCenter);
+ }
+
+ public Date getLastRefreshDate() {
+ return lastRefreshDate > 0 ? new Date(lastRefreshDate) : null;
+ }
+
+ private boolean needsRefresh() {
+ return lastRefreshDate + PERIOD_IN_MILLISECONDS < System.currentTimeMillis();
+ }
+
+ private UpdateCenter init() {
+ InputStream input = null;
+ try {
+ String content = uriReader.readString(uri, StandardCharsets.UTF_8);
+ java.util.Properties properties = new java.util.Properties();
+ input = IOUtils.toInputStream(content, StandardCharsets.UTF_8);
+ properties.load(input);
+ return new UpdateCenterDeserializer(Mode.PROD, true).fromProperties(properties);
+
+ } catch (Exception e) {
+ Loggers.get(getClass()).error("Fail to connect to update center", e);
+ return null;
+
+ } finally {
+ IOUtils.closeQuietly(input);
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java
new file mode 100644
index 00000000000..0c0a6730987
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/UpdateCenterMatrixFactory.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.base.Optional;
+import org.sonar.api.SonarRuntime;
+import org.sonar.updatecenter.common.UpdateCenter;
+import org.sonar.updatecenter.common.Version;
+
+/**
+ * @since 2.4
+ */
+public class UpdateCenterMatrixFactory {
+
+ private final UpdateCenterClient centerClient;
+ private final SonarRuntime sonarRuntime;
+ private final InstalledPluginReferentialFactory installedPluginReferentialFactory;
+
+ public UpdateCenterMatrixFactory(UpdateCenterClient centerClient, SonarRuntime runtime,
+ InstalledPluginReferentialFactory installedPluginReferentialFactory) {
+ this.centerClient = centerClient;
+ this.sonarRuntime = runtime;
+ this.installedPluginReferentialFactory = installedPluginReferentialFactory;
+ }
+
+ public Optional<UpdateCenter> getUpdateCenter(boolean refreshUpdateCenter) {
+ Optional<UpdateCenter> updateCenter = centerClient.getUpdateCenter(refreshUpdateCenter);
+ if (updateCenter.isPresent()) {
+ org.sonar.api.utils.Version fullVersion = sonarRuntime.getApiVersion();
+ org.sonar.api.utils.Version semanticVersion = org.sonar.api.utils.Version.create(fullVersion.major(), fullVersion.minor(), fullVersion.patch());
+
+ return Optional.of(updateCenter.get().setInstalledSonarVersion(Version.create(semanticVersion.toString())).registerInstalledPlugins(
+ installedPluginReferentialFactory.getInstalledPluginReferential())
+ .setDate(centerClient.getLastRefreshDate()));
+ }
+ return Optional.absent();
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/WebServerExtensionInstaller.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/WebServerExtensionInstaller.java
new file mode 100644
index 00000000000..9fa956d2a15
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/WebServerExtensionInstaller.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.platform.PluginRepository;
+
+import static java.util.Collections.singleton;
+
+@ServerSide
+public class WebServerExtensionInstaller extends ServerExtensionInstaller {
+ public WebServerExtensionInstaller(SonarRuntime sonarRuntime, PluginRepository pluginRepository) {
+ super(sonarRuntime, pluginRepository, singleton(ServerSide.class));
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/EditionBundledPlugins.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/EditionBundledPlugins.java
new file mode 100644
index 00000000000..f6c1db3b035
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/EditionBundledPlugins.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins.edition;
+
+import java.util.Arrays;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.updatecenter.common.Plugin;
+
+public final class EditionBundledPlugins {
+
+ private static final String SONARSOURCE_ORGANIZATION = "SonarSource";
+ private static final String[] SONARSOURCE_COMMERCIAL_LICENSES = {"SonarSource", "Commercial"};
+
+ private EditionBundledPlugins() {
+ // prevents instantiation
+ }
+
+ public static boolean isEditionBundled(Plugin plugin) {
+ return SONARSOURCE_ORGANIZATION.equalsIgnoreCase(plugin.getOrganization())
+ && Arrays.stream(SONARSOURCE_COMMERCIAL_LICENSES).anyMatch(s -> s.equalsIgnoreCase(plugin.getLicense()));
+ }
+
+ public static boolean isEditionBundled(PluginInfo pluginInfo) {
+ return SONARSOURCE_ORGANIZATION.equalsIgnoreCase(pluginInfo.getOrganizationName())
+ && Arrays.stream(SONARSOURCE_COMMERCIAL_LICENSES).anyMatch(s -> s.equalsIgnoreCase(pluginInfo.getLicense()));
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/package-info.java
new file mode 100644
index 00000000000..e0a7b00bbf9
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/edition/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.plugins.edition;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/package-info.java
new file mode 100644
index 00000000000..cc398bb88c6
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.plugins;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java
new file mode 100644
index 00000000000..6afdea527e9
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import java.util.Set;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface ProjectLifeCycleListener {
+ /**
+ * This method is called after the specified projects have been deleted.
+ */
+ void onProjectsDeleted(Set<Project> projects);
+
+ /**
+ * This method is called after the specified projects have been deleted.
+ */
+ void onProjectBranchesDeleted(Set<Project> projects);
+
+ /**
+ * This method is called after the specified projects' keys have been modified.
+ */
+ void onProjectsRekeyed(Set<RekeyedProject> rekeyedProjects);
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java
new file mode 100644
index 00000000000..7f3d3f1867d
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import java.util.Set;
+
+public interface ProjectLifeCycleListeners {
+ /**
+ * This method is called after the specified projects have been deleted and will call method
+ * {@link ProjectLifeCycleListener#onProjectsDeleted(Set) onProjectsDeleted(Set)} of all known
+ * {@link ProjectLifeCycleListener} implementations.
+ * <p>
+ * This method ensures all {@link ProjectLifeCycleListener} implementations are called, even if one or more of
+ * them fail with an exception.
+ */
+ void onProjectsDeleted(Set<Project> projects);
+
+ /**
+ * This method is called after the specified project branches have been deleted and will call method
+ * {@link ProjectLifeCycleListener#onProjectBranchesDeleted(Set)} of all known
+ * {@link ProjectLifeCycleListener} implementations.
+ * <p>
+ * This method ensures all {@link ProjectLifeCycleListener} implementations are called, even if one or more of
+ * them fail with an exception.
+ */
+ void onProjectBranchesDeleted(Set<Project> projects);
+
+ /**
+ * This method is called after the specified project's key has been changed and will call method
+ * {@link ProjectLifeCycleListener#onProjectsRekeyed(Set) onProjectsRekeyed(Set)} of all known
+ * {@link ProjectLifeCycleListener} implementations.
+ * <p>
+ * This method ensures all {@link ProjectLifeCycleListener} implementations are called, even if one or more of
+ * them fail with an exception.
+ */
+ void onProjectsRekeyed(Set<RekeyedProject> rekeyedProjects);
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java
new file mode 100644
index 00000000000..af440d60adb
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.function.Consumer;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class ProjectLifeCycleListenersImpl implements ProjectLifeCycleListeners {
+ private static final Logger LOG = Loggers.get(ProjectLifeCycleListenersImpl.class);
+
+ private final ProjectLifeCycleListener[] listeners;
+
+ /**
+ * Used by Pico when there is no ProjectLifeCycleListener implementation in container.
+ */
+ public ProjectLifeCycleListenersImpl() {
+ this.listeners = new ProjectLifeCycleListener[0];
+ }
+
+ /**
+ * Used by Pico when there is at least one ProjectLifeCycleListener implementation in container.
+ */
+ public ProjectLifeCycleListenersImpl(ProjectLifeCycleListener[] listeners) {
+ this.listeners = listeners;
+ }
+
+ @Override
+ public void onProjectsDeleted(Set<Project> projects) {
+ checkNotNull(projects, "projects can't be null");
+ if (projects.isEmpty()) {
+ return;
+ }
+
+ Arrays.stream(listeners)
+ .forEach(safelyCallListener(listener -> listener.onProjectsDeleted(projects)));
+ }
+
+ @Override
+ public void onProjectBranchesDeleted(Set<Project> projects) {
+ checkNotNull(projects, "projects can't be null");
+ if (projects.isEmpty()) {
+ return;
+ }
+
+ Arrays.stream(listeners)
+ .forEach(safelyCallListener(listener -> listener.onProjectBranchesDeleted(projects)));
+ }
+
+ @Override
+ public void onProjectsRekeyed(Set<RekeyedProject> rekeyedProjects) {
+ checkNotNull(rekeyedProjects, "rekeyedProjects can't be null");
+ if (rekeyedProjects.isEmpty()) {
+ return;
+ }
+
+ Arrays.stream(listeners)
+ .forEach(safelyCallListener(listener -> listener.onProjectsRekeyed(rekeyedProjects)));
+ }
+
+ private static Consumer<ProjectLifeCycleListener> safelyCallListener(Consumer<ProjectLifeCycleListener> task) {
+ return listener -> {
+ try {
+ task.accept(listener);
+ } catch (Error | Exception e) {
+ LOG.error("Call on ProjectLifeCycleListener \"{}\" failed", listener.getClass(), e);
+ }
+ };
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/RekeyedProject.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/RekeyedProject.java
new file mode 100644
index 00000000000..ecab0b148f8
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/RekeyedProject.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public final class RekeyedProject {
+ private final Project project;
+ private final String previousKey;
+
+ public RekeyedProject(Project project, String previousKey) {
+ this.project = checkNotNull(project, "project can't be null");
+ this.previousKey = checkNotNull(previousKey, "previousKey can't be null");
+ }
+
+ public Project getProject() {
+ return project;
+ }
+
+ public String getPreviousKey() {
+ return previousKey;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RekeyedProject that = (RekeyedProject) o;
+ return Objects.equals(project, that.project) &&
+ Objects.equals(previousKey, that.previousKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(project, previousKey);
+ }
+
+ @Override
+ public String toString() {
+ return "RekeyedProject{" +
+ "project=" + project +
+ ", previousKey='" + previousKey + '\'' +
+ '}';
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/Visibility.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/Visibility.java
new file mode 100644
index 00000000000..edf05180276
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/Visibility.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import java.util.List;
+
+import static java.util.Arrays.stream;
+import static org.sonar.core.util.stream.MoreCollectors.toList;
+
+public enum Visibility {
+
+ PRIVATE(true, "private"),
+ PUBLIC(false, "public");
+
+ private static final List<String> LABELS = stream(values()).map(Visibility::getLabel).collect(toList(values().length));
+
+ private final boolean isPrivate;
+ private final String label;
+
+ Visibility(boolean isPrivate, String label) {
+ this.isPrivate = isPrivate;
+ this.label = label;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ boolean isPrivate() {
+ return isPrivate;
+ }
+
+ public static String getLabel(boolean isPrivate) {
+ return stream(values())
+ .filter(v -> v.isPrivate == isPrivate)
+ .map(Visibility::getLabel)
+ .findAny()
+ .orElseThrow(() -> new IllegalStateException("Invalid visibility boolean '" + isPrivate + "'"));
+ }
+
+ public static boolean isPrivate(String label) {
+ return parseVisibility(label).isPrivate();
+ }
+
+ public static Visibility parseVisibility(String label) {
+ return stream(values())
+ .filter(v -> v.label.equals(label))
+ .findAny()
+ .orElseThrow(() -> new IllegalStateException("Invalid visibility label '" + label + "'"));
+ }
+
+ public static List<String> getLabels() {
+ return LABELS;
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/project/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/package-info.java
new file mode 100644
index 00000000000..205d7058e06
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/project/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/ProjectsInWarning.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/ProjectsInWarning.java
new file mode 100644
index 00000000000..7591c138d28
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/ProjectsInWarning.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Store number of projects in warning in order for the web service api/components/search to know if warning value should be return in the quality gate facet.
+ * The value is updated each time the daemon {@link ProjectsInWarningDaemon} is executed
+ */
+public class ProjectsInWarning {
+
+ private Long projectsInWarning;
+
+ public void update(long projectsInWarning) {
+ this.projectsInWarning = projectsInWarning;
+ }
+
+ public long count() {
+ checkArgument(isInitialized(), "Initialization has not be done");
+ return projectsInWarning;
+ }
+
+ boolean isInitialized() {
+ return projectsInWarning != null;
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java
new file mode 100644
index 00000000000..6953a77575e
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class QGChangeEvent {
+ private final ComponentDto project;
+ private final BranchDto branch;
+ private final SnapshotDto analysis;
+ private final Configuration projectConfiguration;
+ private final Metric.Level previousStatus;
+ private final Supplier<Optional<EvaluatedQualityGate>> qualityGateSupplier;
+
+ public QGChangeEvent(ComponentDto project, BranchDto branch, SnapshotDto analysis, Configuration projectConfiguration,
+ @Nullable Metric.Level previousStatus, Supplier<Optional<EvaluatedQualityGate>> qualityGateSupplier) {
+ this.project = requireNonNull(project, "project can't be null");
+ this.branch = requireNonNull(branch, "branch can't be null");
+ this.analysis = requireNonNull(analysis, "analysis can't be null");
+ this.projectConfiguration = requireNonNull(projectConfiguration, "projectConfiguration can't be null");
+ this.previousStatus = previousStatus;
+ this.qualityGateSupplier = requireNonNull(qualityGateSupplier, "qualityGateSupplier can't be null");
+ }
+
+ public BranchDto getBranch() {
+ return branch;
+ }
+
+ public ComponentDto getProject() {
+ return project;
+ }
+
+ public SnapshotDto getAnalysis() {
+ return analysis;
+ }
+
+ public Configuration getProjectConfiguration() {
+ return projectConfiguration;
+ }
+
+ public Optional<Metric.Level> getPreviousStatus() {
+ return Optional.ofNullable(previousStatus);
+ }
+
+ public Supplier<Optional<EvaluatedQualityGate>> getQualityGateSupplier() {
+ return qualityGateSupplier;
+ }
+
+ @Override
+ public String toString() {
+ return "QGChangeEvent{" +
+ "project=" + toString(project) +
+ ", branch=" + toString(branch) +
+ ", analysis=" + toString(analysis) +
+ ", projectConfiguration=" + projectConfiguration +
+ ", previousStatus=" + previousStatus +
+ ", qualityGateSupplier=" + qualityGateSupplier +
+ '}';
+ }
+
+ private static String toString(ComponentDto project) {
+ return project.uuid() + ":" + project.getKey();
+ }
+
+ private static String toString(BranchDto branch) {
+ return branch.getBranchType() + ":" + branch.getUuid() + ":" + branch.getProjectUuid() + ":" + branch.getMergeBranchUuid();
+ }
+
+ private static String toString(SnapshotDto analysis) {
+ return analysis.getUuid() + ":" + analysis.getCreatedAt();
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java
new file mode 100644
index 00000000000..c3baf201471
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+import java.util.EnumSet;
+import java.util.Set;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface QGChangeEventListener {
+ /**
+ * Called consequently to a change done on one or more issue of a given project.
+ *
+ * @param qualityGateEvent can not be {@code null}
+ * @param changedIssues can not be {@code null} nor empty
+ */
+ void onIssueChanges(QGChangeEvent qualityGateEvent, Set<ChangedIssue> changedIssues);
+
+ interface ChangedIssue {
+
+ String getKey();
+
+ Status getStatus();
+
+ RuleType getType();
+
+ String getSeverity();
+
+ default boolean isNotClosed() {
+ return !Status.CLOSED_STATUSES.contains(getStatus());
+ }
+ }
+
+ enum Status {
+ OPEN,
+ CONFIRMED,
+ REOPENED,
+ RESOLVED_FP,
+ RESOLVED_WF,
+ RESOLVED_FIXED,
+ TO_REVIEW,
+ IN_REVIEW,
+ REVIEWED;
+
+ protected static final Set<Status> CLOSED_STATUSES = EnumSet.of(CONFIRMED, RESOLVED_FIXED, RESOLVED_FP, RESOLVED_WF);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
new file mode 100644
index 00000000000..e2e66b555de
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+import java.util.Collection;
+import java.util.List;
+import org.sonar.core.issue.DefaultIssue;
+
+public interface QGChangeEventListeners {
+
+ void broadcastOnIssueChange(List<DefaultIssue> changedIssues, Collection<QGChangeEvent> qgChangeEvents);
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
new file mode 100644
index 00000000000..76a4c2bebf1
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
@@ -0,0 +1,180 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+import com.google.common.collect.Multimap;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.qualitygate.changeevent.QGChangeEventListener.ChangedIssue;
+
+import static java.lang.String.format;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+
+/**
+ * Broadcast a given collection of {@link QGChangeEvent} for a specific trigger to all the registered
+ * {@link QGChangeEventListener} in Pico.
+ *
+ * This class ensures that an {@link Exception} occurring calling one of the {@link QGChangeEventListener} doesn't
+ * prevent from calling the others.
+ */
+public class QGChangeEventListenersImpl implements QGChangeEventListeners {
+ private static final Logger LOG = Loggers.get(QGChangeEventListenersImpl.class);
+
+ private final QGChangeEventListener[] listeners;
+
+ /**
+ * Used by Pico when there is no QGChangeEventListener instance in container.
+ */
+ public QGChangeEventListenersImpl() {
+ this.listeners = new QGChangeEventListener[0];
+ }
+
+ public QGChangeEventListenersImpl(QGChangeEventListener[] listeners) {
+ this.listeners = listeners;
+ }
+
+ @Override
+ public void broadcastOnIssueChange(List<DefaultIssue> issues, Collection<QGChangeEvent> changeEvents) {
+ if (listeners.length == 0 || issues.isEmpty() || changeEvents.isEmpty()) {
+ return;
+ }
+
+ try {
+ Multimap<String, QGChangeEvent> eventsByComponentUuid = changeEvents.stream()
+ .collect(MoreCollectors.index(t -> t.getProject().uuid()));
+ Multimap<String, DefaultIssue> issueByComponentUuid = issues.stream()
+ .collect(MoreCollectors.index(DefaultIssue::projectUuid));
+
+ issueByComponentUuid.asMap()
+ .forEach((componentUuid, value) -> {
+ Collection<QGChangeEvent> qgChangeEvents = eventsByComponentUuid.get(componentUuid);
+ if (!qgChangeEvents.isEmpty()) {
+ Set<ChangedIssue> changedIssues = value.stream()
+ .map(ChangedIssueImpl::new)
+ .collect(toSet());
+ qgChangeEvents
+ .forEach(changeEvent -> Arrays.stream(listeners)
+ .forEach(listener -> broadcastTo(changedIssues, changeEvent, listener)));
+ }
+ });
+ } catch (Error e) {
+ LOG.warn(format("Broadcasting to listeners failed for %s events", changeEvents.size()), e);
+ }
+ }
+
+ private static void broadcastTo(Set<ChangedIssue> changedIssues, QGChangeEvent changeEvent, QGChangeEventListener listener) {
+ try {
+ LOG.trace("calling onChange() on listener {} for events {}...", listener.getClass().getName(), changeEvent);
+ listener.onIssueChanges(changeEvent, changedIssues);
+ } catch (Exception e) {
+ LOG.warn(format("onChange() call failed on listener %s for events %s", listener.getClass().getName(), changeEvent), e);
+ }
+ }
+
+ static class ChangedIssueImpl implements ChangedIssue {
+ private final String key;
+ private final QGChangeEventListener.Status status;
+ private final RuleType type;
+ private final String severity;
+
+ ChangedIssueImpl(DefaultIssue issue) {
+ this.key = issue.key();
+ this.status = statusOf(issue);
+ this.type = issue.type();
+ this.severity = issue.severity();
+ }
+
+ static QGChangeEventListener.Status statusOf(DefaultIssue issue) {
+ switch (issue.status()) {
+ case Issue.STATUS_OPEN:
+ return QGChangeEventListener.Status.OPEN;
+ case Issue.STATUS_CONFIRMED:
+ return QGChangeEventListener.Status.CONFIRMED;
+ case Issue.STATUS_REOPENED:
+ return QGChangeEventListener.Status.REOPENED;
+ case Issue.STATUS_TO_REVIEW:
+ return QGChangeEventListener.Status.TO_REVIEW;
+ case Issue.STATUS_IN_REVIEW:
+ return QGChangeEventListener.Status.IN_REVIEW;
+ case Issue.STATUS_REVIEWED:
+ return QGChangeEventListener.Status.REVIEWED;
+ case Issue.STATUS_RESOLVED:
+ return statusOfResolved(issue);
+ default:
+ throw new IllegalStateException("Unexpected status: " + issue.status());
+ }
+ }
+
+ private static QGChangeEventListener.Status statusOfResolved(DefaultIssue issue) {
+ String resolution = issue.resolution();
+ Objects.requireNonNull(resolution, "A resolved issue should have a resolution");
+ switch (resolution) {
+ case Issue.RESOLUTION_FALSE_POSITIVE:
+ return QGChangeEventListener.Status.RESOLVED_FP;
+ case Issue.RESOLUTION_WONT_FIX:
+ return QGChangeEventListener.Status.RESOLVED_WF;
+ case Issue.RESOLUTION_FIXED:
+ return QGChangeEventListener.Status.RESOLVED_FIXED;
+ default:
+ throw new IllegalStateException("Unexpected resolution for a resolved issue: " + resolution);
+ }
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public QGChangeEventListener.Status getStatus() {
+ return status;
+ }
+
+ @Override
+ public RuleType getType() {
+ return type;
+ }
+
+ @Override
+ public String getSeverity() {
+ return severity;
+ }
+
+ @Override
+ public String toString() {
+ return "ChangedIssueImpl{" +
+ "key='" + key + '\'' +
+ ", status=" + status +
+ ", type=" + type +
+ ", severity=" + severity +
+ '}';
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java
new file mode 100644
index 00000000000..6ef8397046a
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+public enum Trigger {
+ ISSUE_CHANGE
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java
new file mode 100644
index 00000000000..ed52cd87a47
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/BulkChangeResult.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/BulkChangeResult.java
new file mode 100644
index 00000000000..91a31ca76d9
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/BulkChangeResult.java
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualityprofile;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class BulkChangeResult {
+
+ private final List<String> errors = new ArrayList<>();
+ private int succeeded = 0;
+ private int failed = 0;
+ private final List<ActiveRuleChange> changes = new ArrayList<>();
+
+ public List<String> getErrors() {
+ return errors;
+ }
+
+ public int countSucceeded() {
+ return succeeded;
+ }
+
+ public int countFailed() {
+ return failed;
+ }
+
+ void incrementSucceeded() {
+ succeeded++;
+ }
+
+ void incrementFailed() {
+ failed++;
+ }
+
+ void addChanges(Collection<ActiveRuleChange> c) {
+ this.changes.addAll(c);
+ }
+
+ public List<ActiveRuleChange> getChanges() {
+ return changes;
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/QProfileRules.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/QProfileRules.java
new file mode 100644
index 00000000000..32a11c072d8
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/QProfileRules.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualityprofile;
+
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbSession;
+import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.rule.index.RuleQuery;
+
+/**
+ * Operations related to activation and deactivation of rules on user profiles.
+ */
+@ServerSide
+public interface QProfileRules {
+
+ /**
+ * Activate multiple rules at once on a Quality profile.
+ * Db session is committed and Elasticsearch indices are updated.
+ * If an activation fails to be executed, then all others are
+ * canceled, db session is not committed and an exception is
+ * thrown.
+ */
+ List<ActiveRuleChange> activateAndCommit(DbSession dbSession, QProfileDto profile, Collection<RuleActivation> activations);
+
+ /**
+ * Same as {@link #activateAndCommit(DbSession, QProfileDto, Collection)} except
+ * that:
+ * - rules are loaded from search engine
+ * - rules are activated with default parameters
+ * - an activation failure does not break others. No exception is thrown.
+ */
+ BulkChangeResult bulkActivateAndCommit(DbSession dbSession, QProfileDto profile, RuleQuery ruleQuery, @Nullable String severity);
+
+ List<ActiveRuleChange> deactivateAndCommit(DbSession dbSession, QProfileDto profile, Collection<Integer> ruleIds);
+
+ BulkChangeResult bulkDeactivateAndCommit(DbSession dbSession, QProfileDto profile, RuleQuery ruleQuery);
+
+ /**
+ * Delete a rule from all Quality profiles. Db session is not committed. As a
+ * consequence Elasticsearch indices are NOT updated.
+ */
+ List<ActiveRuleChange> deleteRule(DbSession dbSession, RuleDefinitionDto rule);
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java
new file mode 100644
index 00000000000..106fd158b26
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualityprofile;
+
+import com.google.common.base.Strings;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.Severity;
+
+/**
+ * The request for activation.
+ */
+@Immutable
+public class RuleActivation {
+
+ private final int ruleId;
+ private final boolean reset;
+ private final String severity;
+ private final Map<String, String> parameters = new HashMap<>();
+
+ private RuleActivation(int ruleId, boolean reset, @Nullable String severity, @Nullable Map<String, String> parameters) {
+ this.ruleId = ruleId;
+ this.reset = reset;
+ this.severity = severity;
+ if (severity != null && !Severity.ALL.contains(severity)) {
+ throw new IllegalArgumentException("Unknown severity: " + severity);
+ }
+ if (parameters != null) {
+ for (Map.Entry<String, String> entry : parameters.entrySet()) {
+ this.parameters.put(entry.getKey(), Strings.emptyToNull(entry.getValue()));
+ }
+ }
+ }
+
+ public static RuleActivation createReset(int ruleId) {
+ return new RuleActivation(ruleId, true, null, null);
+ }
+
+ public static RuleActivation create(int ruleId, @Nullable String severity, @Nullable Map<String, String> parameters) {
+ return new RuleActivation(ruleId, false, severity, parameters);
+ }
+
+ public static RuleActivation create(int ruleId) {
+ return create(ruleId, null, null);
+ }
+
+ /**
+ * Optional severity. Use the parent severity or default rule severity if null.
+ */
+ @CheckForNull
+ public String getSeverity() {
+ return severity;
+ }
+
+ public int getRuleId() {
+ return ruleId;
+ }
+
+ @CheckForNull
+ public String getParameter(String key) {
+ return parameters.get(key);
+ }
+
+ public boolean hasParameter(String key) {
+ return parameters.containsKey(key);
+ }
+
+ public boolean isReset() {
+ return reset;
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/package-info.java
new file mode 100644
index 00000000000..5a3c798af36
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualityprofile;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoader.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoader.java
new file mode 100644
index 00000000000..e261763e6f5
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoader.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import org.sonar.api.config.Configuration;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+public interface ProjectConfigurationLoader {
+ /**
+ * Loads configuration for the specified components.
+ *
+ * <p>
+ * Returns the applicable component configuration with most specific configuration overriding more global ones
+ * (eg. global > project > branch).
+ *
+ * <p>
+ * Any component is accepted but SQ only supports specific properties for projects and branches.
+ */
+ Map<String, Configuration> loadProjectConfigurations(DbSession dbSession, Set<ComponentDto> projects);
+
+ default Configuration loadProjectConfiguration(DbSession dbSession, ComponentDto project) {
+ Map<String, Configuration> configurations = loadProjectConfigurations(dbSession, Collections.singleton(project));
+ return requireNonNull(configurations.get(project.uuid()), () -> format("Configuration for project '%s' is not found", project.getKey()));
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoaderImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoaderImpl.java
new file mode 100644
index 00000000000..95e11242587
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/ProjectConfigurationLoaderImpl.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.Settings;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.property.PropertyDto;
+
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class ProjectConfigurationLoaderImpl implements ProjectConfigurationLoader {
+ private final Settings globalSettings;
+ private final DbClient dbClient;
+
+ public ProjectConfigurationLoaderImpl(Settings globalSettings, DbClient dbClient) {
+ this.globalSettings = globalSettings;
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public Map<String, Configuration> loadProjectConfigurations(DbSession dbSession, Set<ComponentDto> projects) {
+ Set<String> mainBranchDbKeys = projects.stream().map(ComponentDto::getKey).collect(Collectors.toSet());
+ Map<String, ChildSettings> mainBranchSettingsByDbKey = loadMainBranchConfigurations(dbSession, mainBranchDbKeys);
+ return projects.stream()
+ .collect(uniqueIndex(ComponentDto::uuid, component -> {
+ if (component.getDbKey().equals(component.getKey())) {
+ return mainBranchSettingsByDbKey.get(component.getKey()).asConfiguration();
+ }
+
+ ChildSettings settings = new ChildSettings(mainBranchSettingsByDbKey.get(component.getKey()));
+ dbClient.propertiesDao()
+ .selectProjectProperties(dbSession, component.getDbKey())
+ .forEach(property -> settings.setProperty(property.getKey(), property.getValue()));
+ return settings.asConfiguration();
+ }));
+ }
+
+ private Map<String, ChildSettings> loadMainBranchConfigurations(DbSession dbSession, Set<String> dbKeys) {
+ return dbKeys.stream().collect(uniqueIndex(Function.identity(), dbKey -> {
+ ChildSettings settings = new ChildSettings(globalSettings);
+ List<PropertyDto> propertyDtos = dbClient.propertiesDao()
+ .selectProjectProperties(dbSession, dbKey);
+ propertyDtos
+ .forEach(property -> settings.setProperty(property.getKey(), property.getValue()));
+ return settings;
+ }));
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/SettingsChangeNotifier.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/SettingsChangeNotifier.java
new file mode 100644
index 00000000000..de074ef5232
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/SettingsChangeNotifier.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.sonar.api.config.GlobalPropertyChangeHandler;
+
+import javax.annotation.Nullable;
+
+public class SettingsChangeNotifier {
+
+ @VisibleForTesting
+ GlobalPropertyChangeHandler[] changeHandlers;
+
+ public SettingsChangeNotifier(GlobalPropertyChangeHandler[] changeHandlers) {
+ this.changeHandlers = changeHandlers;
+ }
+
+ public SettingsChangeNotifier() {
+ this(new GlobalPropertyChangeHandler[0]);
+ }
+
+ public void onGlobalPropertyChange(String key, @Nullable String value) {
+ GlobalPropertyChangeHandler.PropertyChange change = GlobalPropertyChangeHandler.PropertyChange.create(key, value);
+ for (GlobalPropertyChangeHandler changeHandler : changeHandlers) {
+ changeHandler.onChange(change);
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/package-info.java
new file mode 100644
index 00000000000..e816bf4051c
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/setting/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java
new file mode 100644
index 00000000000..22029d62a36
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.telemetry;
+
+import java.util.Optional;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface LicenseReader {
+ Optional<License> read();
+
+ interface License {
+ String getType();
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/package-info.java
new file mode 100644
index 00000000000..ce3724b93a5
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.telemetry;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/BooleanTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/BooleanTypeValidation.java
new file mode 100644
index 00000000000..1b3ade32e60
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/BooleanTypeValidation.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.PropertyType;
+
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+public class BooleanTypeValidation implements TypeValidation {
+
+ @Override
+ public String key() {
+ return PropertyType.BOOLEAN.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ checkRequest(StringUtils.equalsIgnoreCase(value, "true") || StringUtils.equalsIgnoreCase(value, "false"),
+ "Value '%s' must be one of \"true\" or \"false\".", value);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/FloatTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/FloatTypeValidation.java
new file mode 100644
index 00000000000..fd221dced55
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/FloatTypeValidation.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.api.PropertyType;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static java.lang.String.format;
+
+public class FloatTypeValidation implements TypeValidation {
+
+ @Override
+ public String key() {
+ return PropertyType.FLOAT.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ try {
+ Double.parseDouble(value);
+ } catch (NumberFormatException e) {
+ throw BadRequestException.create(format("Value '%s' must be an floating point number.", value));
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java
new file mode 100644
index 00000000000..81f13b22a1d
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+
+/**
+ * Provide a simple mechanism to manage global locks across multiple nodes running in a cluster.
+ * In the target use case multiple nodes try to execute something at around the same time,
+ * and only the first should succeed, and the rest do nothing.
+ */
+@ComputeEngineSide
+@ServerSide
+public class GlobalLockManager {
+
+ static final int DEFAULT_LOCK_DURATION_SECONDS = 180;
+
+ private final DbClient dbClient;
+
+ public GlobalLockManager(DbClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+ /**
+ * Try to acquire a lock on the given name in the default namespace,
+ * using the generic locking mechanism of {@see org.sonar.db.property.InternalPropertiesDao}.
+ */
+ public boolean tryLock(String name) {
+ return tryLock(name, DEFAULT_LOCK_DURATION_SECONDS);
+ }
+
+ public boolean tryLock(String name, int durationSecond) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ boolean success = dbClient.internalPropertiesDao().tryLock(dbSession, name, durationSecond);
+ dbSession.commit();
+ return success;
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/IntegerTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/IntegerTypeValidation.java
new file mode 100644
index 00000000000..84502df47f7
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/IntegerTypeValidation.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.api.PropertyType;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static java.lang.String.format;
+
+public class IntegerTypeValidation implements TypeValidation {
+
+ @Override
+ public String key() {
+ return PropertyType.INTEGER.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ try {
+ Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw BadRequestException.create(format("Value '%s' must be an integer.", value));
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/LongTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/LongTypeValidation.java
new file mode 100644
index 00000000000..9ca540a1d1d
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/LongTypeValidation.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.api.PropertyType;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static java.lang.String.format;
+
+public class LongTypeValidation implements TypeValidation {
+ @Override
+ public String key() {
+ return PropertyType.LONG.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ try {
+ Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ throw BadRequestException.create(format("Value '%s' must be a long.", value));
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java
new file mode 100644
index 00000000000..ba8598a7786
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.api.PropertyType;
+import org.sonar.api.measures.Metric;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static java.lang.String.format;
+
+public class MetricLevelTypeValidation implements TypeValidation {
+ @Override
+ public String key() {
+ return PropertyType.METRIC_LEVEL.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ try {
+ Metric.Level.valueOf(value);
+ } catch (IllegalArgumentException e) {
+ throw BadRequestException.create(format("Value '%s' must be one of \"OK\", \"ERROR\".", value));
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringListTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringListTypeValidation.java
new file mode 100644
index 00000000000..acb1c7dca8f
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringListTypeValidation.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.PropertyType;
+
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+public class StringListTypeValidation implements TypeValidation {
+
+ @Override
+ public String key() {
+ return PropertyType.SINGLE_SELECT_LIST.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ checkRequest(options == null || options.contains(value), "Value '%s' must be one of : %s.", value, StringUtils.join(options, ", "));
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringTypeValidation.java
new file mode 100644
index 00000000000..3851ede0e81
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/StringTypeValidation.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.sonar.api.PropertyType;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+public class StringTypeValidation implements TypeValidation {
+
+ @Override
+ public String key() {
+ return PropertyType.STRING.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ // Nothing to do
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TextTypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TextTypeValidation.java
new file mode 100644
index 00000000000..c26ad125162
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TextTypeValidation.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.sonar.api.PropertyType;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+public class TextTypeValidation implements TypeValidation {
+
+ @Override
+ public String key() {
+ return PropertyType.TEXT.name();
+ }
+
+ @Override
+ public void validate(String value, @Nullable List<String> options) {
+ // Nothing to do
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidation.java
new file mode 100644
index 00000000000..797a3438d8f
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidation.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.sonar.api.server.ServerSide;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+@ServerSide
+public interface TypeValidation {
+
+ String key();
+
+ void validate(String value, @Nullable List<String> options);
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidationModule.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidationModule.java
new file mode 100644
index 00000000000..3cc4f038e5e
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidationModule.java
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.sonar.core.platform.Module;
+
+public class TypeValidationModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ TypeValidations.class,
+ IntegerTypeValidation.class,
+ FloatTypeValidation.class,
+ BooleanTypeValidation.class,
+ TextTypeValidation.class,
+ StringTypeValidation.class,
+ StringListTypeValidation.class,
+ LongTypeValidation.class,
+ MetricLevelTypeValidation.class
+ );
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidations.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidations.java
new file mode 100644
index 00000000000..2c8883b77eb
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/TypeValidations.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import java.util.List;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.sonar.api.server.ServerSide;
+
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+
+@ServerSide
+public class TypeValidations {
+
+ private final List<TypeValidation> typeValidationList;
+
+ public TypeValidations(List<TypeValidation> typeValidationList) {
+ this.typeValidationList = typeValidationList;
+ }
+
+ public void validate(List<String> values, String type, List<String> options) {
+ TypeValidation typeValidation = findByKey(type);
+ for (String value : values) {
+ typeValidation.validate(value, options);
+ }
+ }
+
+ public void validate(String value, String type, @Nullable List<String> options) {
+ TypeValidation typeValidation = findByKey(type);
+ typeValidation.validate(value, options);
+ }
+
+ private TypeValidation findByKey(String key) {
+ TypeValidation typeValidation = Iterables.find(typeValidationList, new TypeValidationMatchKey(key), null);
+ checkRequest(typeValidation != null, "Type '%s' is not valid.", key);
+ return typeValidation;
+ }
+
+ private static class TypeValidationMatchKey implements Predicate<TypeValidation> {
+ private final String key;
+
+ public TypeValidationMatchKey(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public boolean apply(@Nonnull TypeValidation input) {
+ return input.key().equals(key);
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/Validation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/Validation.java
new file mode 100644
index 00000000000..99d28d0bc96
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/Validation.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+public class Validation {
+
+ public static final String CANT_BE_EMPTY_MESSAGE = "%s can't be empty";
+ public static final String IS_TOO_SHORT_MESSAGE = "%s is too short (minimum is %s characters)";
+ public static final String IS_TOO_LONG_MESSAGE = "%s is too long (maximum is %s characters)";
+ public static final String IS_ALREADY_USED_MESSAGE = "%s has already been taken";
+
+ private Validation() {
+ // only static methods
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/package-info.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/package-info.java
new file mode 100644
index 00000000000..8fd4b5b3582
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/app/ProcessCommandWrapperImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/app/ProcessCommandWrapperImplTest.java
new file mode 100644
index 00000000000..de5f24347fc
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/app/ProcessCommandWrapperImplTest.java
@@ -0,0 +1,158 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.app;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.process.sharedmemoryfile.DefaultProcessCommands;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
+
+public class ProcessCommandWrapperImplTest {
+ private static final int PROCESS_NUMBER = 2;
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private MapSettings settings = new MapSettings();
+
+ @Test
+ public void requestSQRestart_throws_IAE_if_process_index_property_not_set() {
+ ProcessCommandWrapperImpl processCommandWrapper = new ProcessCommandWrapperImpl(settings.asConfig());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Property process.index is not set");
+
+ processCommandWrapper.requestSQRestart();
+ }
+
+ @Test
+ public void requestSQRestart_throws_IAE_if_process_shared_path_property_not_set() {
+ settings.setProperty(PROPERTY_PROCESS_INDEX, 1);
+ ProcessCommandWrapperImpl processCommandWrapper = new ProcessCommandWrapperImpl(settings.asConfig());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Property process.sharedDir is not set");
+
+ processCommandWrapper.requestSQRestart();
+ }
+
+ @Test
+ public void requestSQRestart_updates_shareMemory_file() throws IOException {
+ File tmpDir = temp.newFolder().getAbsoluteFile();
+ settings.setProperty(PROPERTY_SHARED_PATH, tmpDir.getAbsolutePath());
+ settings.setProperty(PROPERTY_PROCESS_INDEX, PROCESS_NUMBER);
+
+ ProcessCommandWrapperImpl underTest = new ProcessCommandWrapperImpl(settings.asConfig());
+ underTest.requestSQRestart();
+
+ try (DefaultProcessCommands processCommands = DefaultProcessCommands.secondary(tmpDir, PROCESS_NUMBER)) {
+ assertThat(processCommands.askedForRestart()).isTrue();
+ }
+ }
+
+ @Test
+ public void requestSQStop_throws_IAE_if_process_shared_path_property_not_set() {
+ settings.setProperty(PROPERTY_PROCESS_INDEX, 1);
+ ProcessCommandWrapperImpl processCommandWrapper = new ProcessCommandWrapperImpl(settings.asConfig());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Property process.sharedDir is not set");
+
+ processCommandWrapper.requestHardStop();
+ }
+
+ @Test
+ public void requestSQStop_updates_shareMemory_file() throws IOException {
+ File tmpDir = temp.newFolder().getAbsoluteFile();
+ settings.setProperty(PROPERTY_SHARED_PATH, tmpDir.getAbsolutePath());
+ settings.setProperty(PROPERTY_PROCESS_INDEX, PROCESS_NUMBER);
+
+ ProcessCommandWrapperImpl underTest = new ProcessCommandWrapperImpl(settings.asConfig());
+ underTest.requestHardStop();
+
+ try (DefaultProcessCommands processCommands = DefaultProcessCommands.secondary(tmpDir, PROCESS_NUMBER)) {
+ assertThat(processCommands.askedForHardStop()).isTrue();
+ }
+ }
+
+ @Test
+ public void notifyOperational_throws_IAE_if_process_sharedDir_property_not_set() {
+ settings.setProperty(PROPERTY_PROCESS_INDEX, 1);
+ ProcessCommandWrapperImpl processCommandWrapper = new ProcessCommandWrapperImpl(settings.asConfig());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Property process.sharedDir is not set");
+
+ processCommandWrapper.notifyOperational();
+ }
+
+ @Test
+ public void notifyOperational_throws_IAE_if_process_index_property_not_set() {
+ ProcessCommandWrapperImpl processCommandWrapper = new ProcessCommandWrapperImpl(settings.asConfig());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Property process.index is not set");
+
+ processCommandWrapper.notifyOperational();
+ }
+
+ @Test
+ public void notifyOperational_updates_shareMemory_file() throws IOException {
+ File tmpDir = temp.newFolder().getAbsoluteFile();
+ settings.setProperty(PROPERTY_SHARED_PATH, tmpDir.getAbsolutePath());
+ settings.setProperty(PROPERTY_PROCESS_INDEX, PROCESS_NUMBER);
+
+ ProcessCommandWrapperImpl underTest = new ProcessCommandWrapperImpl(settings.asConfig());
+ underTest.notifyOperational();
+
+ try (DefaultProcessCommands processCommands = DefaultProcessCommands.secondary(tmpDir, PROCESS_NUMBER)) {
+ assertThat(processCommands.isOperational()).isTrue();
+ }
+ }
+
+ @Test
+ public void isCeOperational_reads_shared_memory_operational_flag_in_location_3() throws IOException {
+ File tmpDir = temp.newFolder().getAbsoluteFile();
+ settings.setProperty(PROPERTY_SHARED_PATH, tmpDir.getAbsolutePath());
+
+ boolean expected = new Random().nextBoolean();
+ if (expected) {
+ try (DefaultProcessCommands processCommands = DefaultProcessCommands.secondary(tmpDir, 3)) {
+ processCommands.setOperational();
+ }
+ }
+
+ ProcessCommandWrapperImpl underTest = new ProcessCommandWrapperImpl(settings.asConfig());
+
+ assertThat(underTest.isCeOperational()).isEqualTo(expected);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureProxyImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureProxyImplTest.java
new file mode 100644
index 00000000000..6fb3d9a6449
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureProxyImplTest.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.branch;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BranchFeatureProxyImplTest {
+
+ private BranchFeatureExtension branchFeatureExtension = mock(BranchFeatureExtension.class);
+
+ @Test
+ public void return_false_when_no_extension() {
+ assertThat(new BranchFeatureProxyImpl().isEnabled()).isFalse();
+ }
+
+ @Test
+ public void return_false_when_extension_returns_false() {
+ when(branchFeatureExtension.isEnabled()).thenReturn(false);
+ assertThat(new BranchFeatureProxyImpl(branchFeatureExtension).isEnabled()).isFalse();
+ }
+
+ @Test
+ public void return_true_when_extension_returns_ftrue() {
+ when(branchFeatureExtension.isEnabled()).thenReturn(true);
+ assertThat(new BranchFeatureProxyImpl(branchFeatureExtension).isEnabled()).isTrue();
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureRule.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureRule.java
new file mode 100644
index 00000000000..2bf56463474
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/branch/BranchFeatureRule.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.branch;
+
+import org.junit.rules.ExternalResource;
+
+public class BranchFeatureRule extends ExternalResource implements BranchFeatureProxy {
+
+ private boolean enabled;
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ protected void after() {
+ reset();
+ }
+
+ public void reset() {
+ this.enabled = false;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/BadRequestExceptionTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/BadRequestExceptionTest.java
new file mode 100644
index 00000000000..0dc82c559f0
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/BadRequestExceptionTest.java
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BadRequestExceptionTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void text_error() {
+ BadRequestException exception = BadRequestException.create("error");
+ assertThat(exception.getMessage()).isEqualTo("error");
+ }
+
+ @Test
+ public void create_exception_from_list() {
+ BadRequestException underTest = BadRequestException.create(asList("error1", "error2"));
+
+ assertThat(underTest.errors()).containsOnly("error1", "error2");
+ }
+
+ @Test
+ public void create_exception_from_var_args() {
+ BadRequestException underTest = BadRequestException.create("error1", "error2");
+
+ assertThat(underTest.errors()).containsOnly("error1", "error2");
+ }
+
+ @Test
+ public void getMessage_return_first_error() {
+ BadRequestException underTest = BadRequestException.create(asList("error1", "error2"));
+
+ assertThat(underTest.getMessage()).isEqualTo("error1");
+ }
+
+ @Test
+ public void fail_when_creating_exception_with_empty_list() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("At least one error message is required");
+
+ BadRequestException.create(Collections.emptyList());
+ }
+
+ @Test
+ public void fail_when_creating_exception_with_one_empty_element() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Message cannot be empty");
+
+ BadRequestException.create(asList("error", ""));
+ }
+
+ @Test
+ public void fail_when_creating_exception_with_one_null_element() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Message cannot be empty");
+
+ BadRequestException.create(asList("error", null));
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/MessageTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/MessageTest.java
new file mode 100644
index 00000000000..f05bb55058a
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/MessageTest.java
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MessageTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void create_message() {
+ Message message = Message.of("key1 %s", "param1");
+ assertThat(message.getMessage()).isEqualTo("key1 param1");
+ }
+
+ @Test
+ public void create_message_without_params() {
+ Message message = Message.of("key1");
+ assertThat(message.getMessage()).isEqualTo("key1");
+ }
+
+ @Test
+ public void fail_when_message_is_null() {
+ expectedException.expect(IllegalArgumentException.class);
+
+ Message.of(null);
+ }
+
+ @Test
+ public void fail_when_message_is_empty() {
+ expectedException.expect(IllegalArgumentException.class);
+
+ Message.of("");
+ }
+
+ @Test
+ public void test_equals_and_hashcode() {
+ Message message1 = Message.of("key1%s", "param1");
+ Message message2 = Message.of("key2%s", "param2");
+ Message message3 = Message.of("key1");
+ Message message4 = Message.of("key1%s", "param2");
+ Message sameAsMessage1 = Message.of("key1%s", "param1");
+
+ assertThat(message1).isEqualTo(message1);
+ assertThat(message1).isNotEqualTo(message2);
+ assertThat(message1).isNotEqualTo(message3);
+ assertThat(message1).isNotEqualTo(message4);
+ assertThat(message1).isEqualTo(sameAsMessage1);
+ assertThat(message1).isNotEqualTo(null);
+ assertThat(message1).isNotEqualTo(new Object());
+
+ assertThat(message1.hashCode()).isEqualTo(message1.hashCode());
+ assertThat(message1.hashCode()).isNotEqualTo(message2.hashCode());
+ assertThat(message1.hashCode()).isNotEqualTo(message3.hashCode());
+ assertThat(message1.hashCode()).isNotEqualTo(message4.hashCode());
+ assertThat(message1.hashCode()).isEqualTo(sameAsMessage1.hashCode());
+ }
+
+ @Test
+ public void to_string() {
+ assertThat(Message.of("key1 %s", "param1").toString()).isEqualTo("key1 param1");
+ assertThat(Message.of("key1").toString()).isEqualTo("key1");
+ assertThat(Message.of("key1", (Object[])null).toString()).isEqualTo("key1");
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/ServerExceptionTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/ServerExceptionTest.java
new file mode 100644
index 00000000000..ae2ce7846db
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/ServerExceptionTest.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.exceptions;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ServerExceptionTest {
+
+ @Test
+ public void should_create_exception_with_status() {
+ ServerException exception = new ServerException(400, "error!");
+ assertThat(exception.httpCode()).isEqualTo(400);
+ }
+
+ @Test
+ public void should_create_exception_with_status_and_message() {
+ ServerException exception = new ServerException(404, "Not found");
+ assertThat(exception.httpCode()).isEqualTo(404);
+ assertThat(exception.getMessage()).isEqualTo("Not found");
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/health/TestStandaloneHealthChecker.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/health/TestStandaloneHealthChecker.java
new file mode 100644
index 00000000000..27c2e469dde
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/health/TestStandaloneHealthChecker.java
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.health;
+
+public class TestStandaloneHealthChecker implements HealthChecker {
+
+ private Health health = Health.newHealthCheckBuilder().setStatus(Health.Status.GREEN).build();
+
+ public void setHealth(Health h) {
+ this.health = h;
+ }
+
+ @Override
+ public Health checkNode() {
+ return health;
+ }
+
+ @Override
+ public ClusterHealth checkCluster() {
+ throw new IllegalStateException();
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java
new file mode 100644
index 00000000000..f984cd9bd1e
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginDownloaderTest.java
@@ -0,0 +1,322 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.base.Optional;
+import java.io.File;
+import java.net.URI;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentMatcher;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.utils.HttpDownloader;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.updatecenter.common.Plugin;
+import org.sonar.updatecenter.common.Release;
+import org.sonar.updatecenter.common.UpdateCenter;
+import org.sonar.updatecenter.common.Version;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.apache.commons.io.FileUtils.copyFileToDirectory;
+import static org.apache.commons.io.FileUtils.touch;
+import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.updatecenter.common.Version.create;
+
+public class PluginDownloaderTest {
+
+ @Rule
+ public TemporaryFolder testFolder = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ private File downloadDir;
+ private UpdateCenterMatrixFactory updateCenterMatrixFactory;
+ private UpdateCenter updateCenter;
+ private HttpDownloader httpDownloader;
+ private PluginDownloader pluginDownloader;
+
+ @Before
+ public void before() throws Exception {
+ updateCenterMatrixFactory = mock(UpdateCenterMatrixFactory.class);
+ updateCenter = mock(UpdateCenter.class);
+ when(updateCenterMatrixFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.of(updateCenter));
+
+ httpDownloader = mock(HttpDownloader.class);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock inv) throws Throwable {
+ File toFile = (File) inv.getArguments()[1];
+ touch(toFile);
+ return null;
+ }
+ }).when(httpDownloader).download(any(URI.class), any(File.class));
+
+ ServerFileSystem fs = mock(ServerFileSystem.class);
+ downloadDir = testFolder.newFolder("downloads");
+ when(fs.getDownloadedPluginsDir()).thenReturn(downloadDir);
+
+ pluginDownloader = new PluginDownloader(updateCenterMatrixFactory, httpDownloader, fs);
+ }
+
+ @After
+ public void stop() {
+ pluginDownloader.stop();
+ }
+
+ @Test
+ public void clean_temporary_files_at_startup() throws Exception {
+ touch(new File(downloadDir, "sonar-php.jar"));
+ touch(new File(downloadDir, "sonar-js.jar.tmp"));
+ assertThat(downloadDir.listFiles()).hasSize(2);
+ pluginDownloader.start();
+
+ File[] files = downloadDir.listFiles();
+ assertThat(files).hasSize(1);
+ assertThat(files[0].getName()).isEqualTo("sonar-php.jar");
+ }
+
+ @Test
+ public void download_from_url() {
+ Plugin test = Plugin.factory("test");
+ Release test10 = new Release(test, "1.0").setDownloadUrl("http://server/test-1.0.jar");
+ test.addRelease(test10);
+
+ when(updateCenter.findInstallablePlugins("foo", create("1.0"))).thenReturn(newArrayList(test10));
+
+ pluginDownloader.start();
+ pluginDownloader.download("foo", create("1.0"));
+
+ // SONAR-4523: do not corrupt JAR files when restarting the server while a plugin is being downloaded.
+ // The JAR file is downloaded in a temp file
+ verify(httpDownloader).download(any(URI.class), argThat(new HasFileName("test-1.0.jar.tmp")));
+ assertThat(new File(downloadDir, "test-1.0.jar")).exists();
+ assertThat(new File(downloadDir, "test-1.0.jar.tmp")).doesNotExist();
+ }
+
+ @Test
+ public void download_when_update_center_is_unavailable_with_no_exception_thrown() {
+ when(updateCenterMatrixFactory.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent());
+
+ Plugin test = Plugin.factory("test");
+ Release test10 = new Release(test, "1.0").setDownloadUrl("http://server/test-1.0.jar");
+ test.addRelease(test10);
+
+ pluginDownloader.start();
+ pluginDownloader.download("foo", create("1.0"));
+ }
+
+ /**
+ * SONAR-4685
+ */
+ @Test
+ public void download_from_redirect_url() {
+ Plugin test = Plugin.factory("plugintest");
+ Release test10 = new Release(test, "1.0").setDownloadUrl("http://server/redirect?r=release&g=test&a=test&v=1.0&e=jar");
+ test.addRelease(test10);
+
+ when(updateCenter.findInstallablePlugins("foo", create("1.0"))).thenReturn(newArrayList(test10));
+
+ pluginDownloader.start();
+ pluginDownloader.download("foo", create("1.0"));
+
+ // SONAR-4523: do not corrupt JAR files when restarting the server while a plugin is being downloaded.
+ // The JAR file is downloaded in a temp file
+ verify(httpDownloader).download(any(URI.class), argThat(new HasFileName("plugintest-1.0.jar.tmp")));
+ assertThat(new File(downloadDir, "plugintest-1.0.jar")).exists();
+ assertThat(new File(downloadDir, "plugintest-1.0.jar.tmp")).doesNotExist();
+ }
+
+ @Test
+ public void throw_exception_if_download_dir_is_invalid() throws Exception {
+ ServerFileSystem fs = mock(ServerFileSystem.class);
+ // download dir is a file instead of being a directory
+ File downloadDir = testFolder.newFile();
+ when(fs.getDownloadedPluginsDir()).thenReturn(downloadDir);
+
+ pluginDownloader = new PluginDownloader(updateCenterMatrixFactory, httpDownloader, fs);
+ try {
+ pluginDownloader.start();
+ fail();
+ } catch (IllegalStateException e) {
+ // ok
+ }
+ }
+
+ @Test
+ public void fail_if_no_compatible_plugin_found() {
+ expectedException.expect(BadRequestException.class);
+
+ pluginDownloader.download("foo", create("1.0"));
+ }
+
+ @Test
+ public void download_from_file() throws Exception {
+ Plugin test = Plugin.factory("test");
+ File file = testFolder.newFile("test-1.0.jar");
+ file.createNewFile();
+ Release test10 = new Release(test, "1.0").setDownloadUrl("file://" + separatorsToUnix(file.getCanonicalPath()));
+ test.addRelease(test10);
+
+ when(updateCenter.findInstallablePlugins("foo", create("1.0"))).thenReturn(newArrayList(test10));
+
+ pluginDownloader.start();
+ pluginDownloader.download("foo", create("1.0"));
+ verify(httpDownloader, never()).download(any(URI.class), any(File.class));
+ assertThat(noDownloadedFiles()).isGreaterThan(0);
+ }
+
+ @Test
+ public void throw_exception_if_could_not_download() {
+ Plugin test = Plugin.factory("test");
+ Release test10 = new Release(test, "1.0").setDownloadUrl("file://not_found");
+ test.addRelease(test10);
+
+ when(updateCenter.findInstallablePlugins("foo", create("1.0"))).thenReturn(newArrayList(test10));
+
+ pluginDownloader.start();
+ try {
+ pluginDownloader.download("foo", create("1.0"));
+ fail();
+ } catch (IllegalStateException e) {
+ // ok
+ }
+ }
+
+ @Test
+ public void throw_exception_if_download_fail() {
+ Plugin test = Plugin.factory("test");
+ Release test10 = new Release(test, "1.0").setDownloadUrl("http://server/test-1.0.jar");
+ test.addRelease(test10);
+ when(updateCenter.findInstallablePlugins("foo", create("1.0"))).thenReturn(newArrayList(test10));
+
+ doThrow(new RuntimeException()).when(httpDownloader).download(any(URI.class), any(File.class));
+
+ pluginDownloader.start();
+ try {
+ pluginDownloader.download("foo", create("1.0"));
+ fail();
+ } catch (IllegalStateException e) {
+ // ok
+ }
+ }
+
+ @Test
+ public void read_download_folder() throws Exception {
+ pluginDownloader.start();
+ assertThat(noDownloadedFiles()).isZero();
+
+ copyFileToDirectory(TestProjectUtils.jarOf("test-base-plugin"), downloadDir);
+
+ assertThat(pluginDownloader.getDownloadedPlugins()).hasSize(1);
+ PluginInfo info = pluginDownloader.getDownloadedPlugins().iterator().next();
+ assertThat(info.getKey()).isEqualTo("testbase");
+ assertThat(info.getName()).isEqualTo("Base Plugin");
+ assertThat(info.getVersion()).isEqualTo(Version.create("0.1-SNAPSHOT"));
+ assertThat(info.getMainClass()).isEqualTo("BasePlugin");
+ }
+
+ @Test
+ public void getDownloadedPluginFilenames_reads_plugin_info_of_files_in_download_folder() throws Exception {
+ pluginDownloader.start();
+ assertThat(pluginDownloader.getDownloadedPlugins()).hasSize(0);
+
+ File file1 = new File(downloadDir, "file1.jar");
+ file1.createNewFile();
+ File file2 = new File(downloadDir, "file2.jar");
+ file2.createNewFile();
+
+ assertThat(noDownloadedFiles()).isEqualTo(2);
+ }
+
+ @Test
+ public void cancel_downloads() throws Exception {
+ File file1 = new File(downloadDir, "file1.jar");
+ file1.createNewFile();
+ File file2 = new File(downloadDir, "file2.jar");
+ file2.createNewFile();
+
+ pluginDownloader.start();
+ assertThat(noDownloadedFiles()).isGreaterThan(0);
+ pluginDownloader.cancelDownloads();
+ assertThat(noDownloadedFiles()).isZero();
+ }
+
+ private int noDownloadedFiles() {
+ return downloadDir.listFiles((file, name) -> name.endsWith(".jar")).length;
+ }
+
+ // SONAR-5011
+ @Test
+ public void download_common_transitive_dependency() {
+ Plugin test1 = Plugin.factory("test1");
+ Release test1R = new Release(test1, "1.0").setDownloadUrl("http://server/test1-1.0.jar");
+ test1.addRelease(test1R);
+
+ Plugin test2 = Plugin.factory("test2");
+ Release test2R = new Release(test2, "1.0").setDownloadUrl("http://server/test2-1.0.jar");
+ test2.addRelease(test2R);
+
+ Plugin testDep = Plugin.factory("testdep");
+ Release testDepR = new Release(testDep, "1.0").setDownloadUrl("http://server/testdep-1.0.jar");
+ testDep.addRelease(testDepR);
+
+ when(updateCenter.findInstallablePlugins("test1", create("1.0"))).thenReturn(newArrayList(test1R, testDepR));
+ when(updateCenter.findInstallablePlugins("test2", create("1.0"))).thenReturn(newArrayList(test2R, testDepR));
+
+ pluginDownloader.start();
+ pluginDownloader.download("test1", create("1.0"));
+ pluginDownloader.download("test2", create("1.0"));
+
+ assertThat(new File(downloadDir, "test1-1.0.jar")).exists();
+ assertThat(new File(downloadDir, "test2-1.0.jar")).exists();
+ assertThat(new File(downloadDir, "testdep-1.0.jar")).exists();
+ }
+
+ class HasFileName implements ArgumentMatcher<File> {
+ private final String name;
+
+ HasFileName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean matches(File file) {
+ return file.getName().equals(name);
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java
new file mode 100644
index 00000000000..bc353f99778
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginFileSystemTest.java
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.core.platform.PluginInfo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.plugins.PluginFileSystem.PROPERTY_PLUGIN_COMPRESSION_ENABLE;
+
+public class PluginFileSystemTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private MapSettings settings = new MapSettings();
+ private Path targetJarPath;
+ private Path targetFolder;
+ private Path sourceFolder;
+
+ @Before
+ public void setUp() throws IOException {
+ sourceFolder = temp.newFolder("source").toPath();
+ targetFolder = temp.newFolder("target").toPath();
+ targetJarPath = targetFolder.resolve("test.jar");
+ Files.createFile(targetJarPath);
+ }
+
+ @Test
+ public void add_plugin_to_list_of_installed_plugins() throws IOException {
+ File jar = touch(temp.newFolder(), "sonar-foo-plugin.jar");
+ PluginInfo info = new PluginInfo("foo");
+
+ PluginFileSystem underTest = new PluginFileSystem(settings.asConfig());
+ underTest.addInstalledPlugin(info, jar);
+
+ assertThat(underTest.getInstalledFiles()).hasSize(1);
+ InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get();
+ assertThat(installedPlugin.getCompressedJar()).isNull();
+ assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(jar.toPath());
+ assertThat(installedPlugin.getPluginInfo()).isSameAs(info);
+ }
+
+ @Test
+ public void compress_jar_if_compression_enabled() throws IOException {
+ File jar = touch(temp.newFolder(), "sonar-foo-plugin.jar");
+ PluginInfo info = new PluginInfo("foo").setJarFile(jar);
+ // the JAR is copied somewhere else in order to be loaded by classloaders
+ File loadedJar = touch(temp.newFolder(), "sonar-foo-plugin.jar");
+
+ settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, true);
+ PluginFileSystem underTest = new PluginFileSystem(settings.asConfig());
+ underTest.addInstalledPlugin(info, loadedJar);
+
+ assertThat(underTest.getInstalledFiles()).hasSize(1);
+
+ InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get();
+ assertThat(installedPlugin.getPluginInfo()).isSameAs(info);
+ assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath());
+ assertThat(installedPlugin.getCompressedJar().getFile())
+ .exists()
+ .isFile()
+ .hasName("sonar-foo-plugin.pack.gz")
+ .hasParent(loadedJar.getParentFile());
+ }
+
+ @Test
+ public void copy_and_use_existing_packed_jar_if_compression_enabled() throws IOException {
+ File jar = touch(temp.newFolder(), "sonar-foo-plugin.jar");
+ File packedJar = touch(jar.getParentFile(), "sonar-foo-plugin.pack.gz");
+ PluginInfo info = new PluginInfo("foo").setJarFile(jar);
+ // the JAR is copied somewhere else in order to be loaded by classloaders
+ File loadedJar = touch(temp.newFolder(), "sonar-foo-plugin.jar");
+
+ settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, true);
+ PluginFileSystem underTest = new PluginFileSystem(settings.asConfig());
+ underTest.addInstalledPlugin(info, loadedJar);
+
+ assertThat(underTest.getInstalledFiles()).hasSize(1);
+
+ InstalledPlugin installedPlugin = underTest.getInstalledPlugin("foo").get();
+ assertThat(installedPlugin.getPluginInfo()).isSameAs(info);
+ assertThat(installedPlugin.getLoadedJar().getFile().toPath()).isEqualTo(loadedJar.toPath());
+ assertThat(installedPlugin.getCompressedJar().getFile())
+ .exists()
+ .isFile()
+ .hasName(packedJar.getName())
+ .hasParent(loadedJar.getParentFile())
+ .hasSameContentAs(packedJar);
+ }
+
+ private static File touch(File dir, String filename) throws IOException {
+ File file = new File(dir, filename);
+ FileUtils.write(file, RandomStringUtils.random(10));
+ return file;
+ }
+
+ //
+ // @Test
+ // public void should_use_deployed_packed_file() throws IOException {
+ // Path packedPath = sourceFolder.resolve("test.pack.gz");
+ // Files.write(packedPath, new byte[] {1, 2, 3});
+ //
+ // settings.setProperty(PROPERTY_PLUGIN_COMPRESSION_ENABLE, true);
+ // underTest = new PluginFileSystem(settings.asConfig());
+ // underTest.compressJar("key", sourceFolder, targetJarPath);
+ //
+ // assertThat(Files.list(targetFolder)).containsOnly(targetJarPath, targetFolder.resolve("test.pack.gz"));
+ // assertThat(underTest.getPlugins()).hasSize(1);
+ // assertThat(underTest.getPlugins().get("key").getFilename()).isEqualTo("test.pack.gz");
+ //
+ // // check that the file was copied, not generated
+ // assertThat(targetFolder.resolve("test.pack.gz")).hasSameContentAs(packedPath);
+ // }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java
new file mode 100644
index 00000000000..20eae5f2781
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/PluginUninstallerTest.java
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.server.platform.ServerFileSystem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class PluginUninstallerTest {
+ @Rule
+ public TemporaryFolder testFolder = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ private File uninstallDir;
+ private PluginUninstaller underTest;
+ private ServerPluginRepository serverPluginRepository;
+ private ServerFileSystem fs;
+
+ @Before
+ public void setUp() throws IOException {
+ serverPluginRepository = mock(ServerPluginRepository.class);
+ uninstallDir = testFolder.newFolder("uninstall");
+ fs = mock(ServerFileSystem.class);
+ when(fs.getUninstalledPluginsDir()).thenReturn(uninstallDir);
+ underTest = new PluginUninstaller(serverPluginRepository, fs);
+ }
+
+ @Test
+ public void uninstall() {
+ when(serverPluginRepository.hasPlugin("plugin")).thenReturn(true);
+ underTest.uninstall("plugin");
+ verify(serverPluginRepository).uninstall("plugin", uninstallDir);
+ }
+
+ @Test
+ public void fail_uninstall_if_plugin_not_installed() {
+ when(serverPluginRepository.hasPlugin("plugin")).thenReturn(false);
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Plugin [plugin] is not installed");
+ underTest.uninstall("plugin");
+ verifyZeroInteractions(serverPluginRepository);
+ }
+
+ @Test
+ public void create_uninstall_dir() {
+ File dir = new File(testFolder.getRoot(), "dir");
+ when(fs.getUninstalledPluginsDir()).thenReturn(dir);
+ underTest = new PluginUninstaller(serverPluginRepository, fs);
+ underTest.start();
+ assertThat(dir).isDirectory();
+ }
+
+ @Test
+ public void cancel() {
+ underTest.cancelUninstalls();
+ verify(serverPluginRepository).cancelUninstalls(uninstallDir);
+ verifyNoMoreInteractions(serverPluginRepository);
+ }
+
+ @Test
+ public void list_uninstalled_plugins() throws IOException {
+ new File(uninstallDir, "file1").createNewFile();
+ copyTestPluginTo("test-base-plugin", uninstallDir);
+ assertThat(underTest.getUninstalledPlugins()).extracting("key").containsOnly("testbase");
+ }
+
+ private File copyTestPluginTo(String testPluginName, File toDir) throws IOException {
+ File jar = TestProjectUtils.jarOf(testPluginName);
+ // file is copied because it's supposed to be moved by the test
+ FileUtils.copyFileToDirectory(jar, toDir);
+ return new File(toDir, jar.getName());
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java
new file mode 100644
index 00000000000..8f4d928cd4c
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.io.File;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.core.platform.ExplodedPlugin;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.server.platform.ServerFileSystem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ServerPluginJarExploderTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private ServerFileSystem fs = mock(ServerFileSystem.class);
+ private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class);
+ private ServerPluginJarExploder underTest = new ServerPluginJarExploder(fs, pluginFileSystem);
+
+ @Test
+ public void copy_all_classloader_files_to_dedicated_directory() throws Exception {
+ File deployDir = temp.newFolder();
+ when(fs.getDeployedPluginsDir()).thenReturn(deployDir);
+ File sourceJar = TestProjectUtils.jarOf("test-libs-plugin");
+ PluginInfo info = PluginInfo.create(sourceJar);
+
+ ExplodedPlugin exploded = underTest.explode(info);
+
+ // all the files loaded by classloaders (JAR + META-INF/libs/*.jar) are copied to the dedicated directory
+ // web/deploy/{pluginKey}
+ File pluginDeployDir = new File(deployDir, "testlibs");
+
+ assertThat(exploded.getKey()).isEqualTo("testlibs");
+ assertThat(exploded.getMain()).isFile().exists().hasParent(pluginDeployDir);
+ assertThat(exploded.getLibs()).extracting("name").containsOnly("commons-daemon-1.0.15.jar", "commons-email-20030310.165926.jar");
+ for (File lib : exploded.getLibs()) {
+ assertThat(lib).exists().isFile();
+ assertThat(lib.getCanonicalPath()).startsWith(pluginDeployDir.getCanonicalPath());
+ }
+ File targetJar = new File(fs.getDeployedPluginsDir(), "testlibs/test-libs-plugin-0.1-SNAPSHOT.jar");
+ verify(pluginFileSystem).addInstalledPlugin(info, targetJar);
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java
new file mode 100644
index 00000000000..e426c9ebfea
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java
@@ -0,0 +1,385 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginLoader;
+import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.updatecenter.common.Version;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ServerPluginRepositoryTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public LogTester logs = new LogTester();
+
+ private SonarRuntime runtime = mock(SonarRuntime.class);
+ private ServerFileSystem fs = mock(ServerFileSystem.class, Mockito.RETURNS_DEEP_STUBS);
+ private PluginLoader pluginLoader = mock(PluginLoader.class);
+ private ServerPluginRepository underTest = new ServerPluginRepository(runtime, fs, pluginLoader);
+
+ @Before
+ public void setUp() throws IOException {
+ when(fs.getDeployedPluginsDir()).thenReturn(temp.newFolder());
+ when(fs.getDownloadedPluginsDir()).thenReturn(temp.newFolder());
+ when(fs.getHomeDir()).thenReturn(temp.newFolder());
+ when(fs.getInstalledPluginsDir()).thenReturn(temp.newFolder());
+ when(fs.getTempDir()).thenReturn(temp.newFolder());
+ when(runtime.getApiVersion()).thenReturn(org.sonar.api.utils.Version.parse("5.2"));
+ }
+
+ @After
+ public void tearDown() {
+ underTest.stop();
+ }
+
+ @Test
+ public void standard_startup_loads_installed_plugins() throws Exception {
+ copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+
+ underTest.start();
+
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase");
+ }
+
+ @Test
+ public void no_plugins_at_all_on_startup() {
+ underTest.start();
+
+ assertThat(underTest.getPluginInfos()).isEmpty();
+ assertThat(underTest.getPluginInfosByKeys()).isEmpty();
+ assertThat(underTest.hasPlugin("testbase")).isFalse();
+ }
+
+ @Test
+ public void fail_if_multiple_jars_for_same_installed_plugin_on_startup() throws Exception {
+ copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ copyTestPluginTo("test-base-plugin-v2", fs.getInstalledPluginsDir());
+
+ try {
+ underTest.start();
+ fail();
+ } catch (MessageException e) {
+ assertThat(e)
+ .hasMessageStartingWith("Found two versions of the plugin Base Plugin [testbase] in the directory extensions/plugins. Please remove one of ")
+ // order is not guaranteed, so assertion is split
+ .hasMessageContaining("test-base-plugin-0.1-SNAPSHOT.jar")
+ .hasMessageContaining("test-base-plugin-0.2-SNAPSHOT.jar");
+ }
+ }
+
+ @Test
+ public void install_downloaded_plugins_on_startup() throws Exception {
+ File downloadedJar = copyTestPluginTo("test-base-plugin", fs.getDownloadedPluginsDir());
+
+ underTest.start();
+
+ // plugin is moved to extensions/plugins then loaded
+ assertThat(downloadedJar).doesNotExist();
+ assertThat(new File(fs.getInstalledPluginsDir(), downloadedJar.getName())).isFile().exists();
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase");
+ }
+
+ @Test
+ public void downloaded_file_overrides_existing_installed_file_on_startup() throws Exception {
+ File installedV1 = copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ File downloadedV2 = copyTestPluginTo("test-base-plugin-v2", fs.getDownloadedPluginsDir());
+
+ underTest.start();
+
+ // plugin is moved to extensions/plugins and replaces v1
+ assertThat(downloadedV2).doesNotExist();
+ assertThat(installedV1).doesNotExist();
+ assertThat(new File(fs.getInstalledPluginsDir(), downloadedV2.getName())).exists();
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase");
+ assertThat(underTest.getPluginInfo("testbase").getVersion()).isEqualTo(Version.create("0.2-SNAPSHOT"));
+ }
+
+ @Test
+ public void blacklisted_plugin_is_automatically_uninstalled_on_startup() throws Exception {
+ underTest.setBlacklistedPluginKeys(ImmutableSet.of("testbase", "issuesreport"));
+ File jar = copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+
+ underTest.start();
+
+ // plugin is not installed and file is deleted
+ assertThat(underTest.getPluginInfos()).isEmpty();
+ assertThat(jar).doesNotExist();
+ }
+
+ @Test
+ public void test_plugin_requirements_at_startup() throws Exception {
+ copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ copyTestPluginTo("test-require-plugin", fs.getInstalledPluginsDir());
+
+ underTest.start();
+
+ // both plugins are installed
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase", "testrequire");
+ }
+
+ @Test
+ public void plugin_is_ignored_if_required_plugin_is_missing_at_startup() throws Exception {
+ copyTestPluginTo("test-require-plugin", fs.getInstalledPluginsDir());
+
+ underTest.start();
+
+ // plugin is not installed as test-base-plugin is missing
+ assertThat(underTest.getPluginInfosByKeys()).isEmpty();
+ }
+
+ @Test
+ public void plugin_is_ignored_if_required_plugin_is_too_old_at_startup() throws Exception {
+ copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ copyTestPluginTo("test-requirenew-plugin", fs.getInstalledPluginsDir());
+
+ underTest.start();
+
+ // the plugin "requirenew" is not installed as it requires base 0.2+ to be installed.
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase");
+ }
+
+ @Test
+ public void fail_if_plugin_does_not_support_sq_version() throws Exception {
+ when(runtime.getApiVersion()).thenReturn(org.sonar.api.utils.Version.parse("1.0"));
+ copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+
+ try {
+ underTest.start();
+ fail();
+ } catch (MessageException e) {
+ assertThat(e).hasMessage("Plugin Base Plugin [testbase] requires at least SonarQube 4.5.4");
+ }
+ }
+
+ @Test
+ public void uninstall() throws Exception {
+ File installedJar = copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ File uninstallDir = temp.newFolder("uninstallDir");
+
+ underTest.start();
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase");
+ underTest.uninstall("testbase", uninstallDir);
+
+ assertThat(installedJar).doesNotExist();
+ // still up. Will be dropped after next startup
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase");
+ assertThat(uninstallDir.list()).containsOnly(installedJar.getName());
+ }
+
+ @Test
+ public void uninstall_dependents() throws Exception {
+ File base = copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ File extension = copyTestPluginTo("test-require-plugin", fs.getInstalledPluginsDir());
+ File uninstallDir = temp.newFolder("uninstallDir");
+
+ underTest.start();
+ assertThat(underTest.getPluginInfos()).hasSize(2);
+ underTest.uninstall("testbase", uninstallDir);
+ assertThat(base).doesNotExist();
+ assertThat(extension).doesNotExist();
+ assertThat(uninstallDir.list()).containsOnly(base.getName(), extension.getName());
+ }
+
+ @Test
+ public void dont_uninstall_non_existing_dependents() throws IOException {
+ File base = copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ File extension = copyTestPluginTo("test-require-plugin", fs.getInstalledPluginsDir());
+ File uninstallDir = temp.newFolder("uninstallDir");
+
+ underTest.start();
+ assertThat(underTest.getPluginInfos()).hasSize(2);
+ underTest.uninstall("testrequire", uninstallDir);
+ assertThat(underTest.getPluginInfos()).hasSize(2);
+
+ underTest.uninstall("testbase", uninstallDir);
+ assertThat(base).doesNotExist();
+ assertThat(extension).doesNotExist();
+ assertThat(uninstallDir.list()).containsOnly(base.getName(), extension.getName());
+ }
+
+ @Test
+ public void dont_uninstall_non_existing_files() throws IOException {
+ File base = copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ File extension = copyTestPluginTo("test-require-plugin", fs.getInstalledPluginsDir());
+ File uninstallDir = temp.newFolder("uninstallDir");
+
+ underTest.start();
+ assertThat(underTest.getPluginInfos()).hasSize(2);
+ underTest.uninstall("testbase", uninstallDir);
+ assertThat(underTest.getPluginInfos()).hasSize(2);
+
+ underTest.uninstall("testbase", uninstallDir);
+ assertThat(base).doesNotExist();
+ assertThat(extension).doesNotExist();
+ assertThat(uninstallDir.list()).containsOnly(base.getName(), extension.getName());
+ }
+
+ @Test
+ public void install_plugin_and_its_extension_plugins_at_startup() throws Exception {
+ copyTestPluginTo("test-base-plugin", fs.getInstalledPluginsDir());
+ copyTestPluginTo("test-extend-plugin", fs.getInstalledPluginsDir());
+
+ underTest.start();
+
+ // both plugins are installed
+ assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase", "testextend");
+ }
+
+ @Test
+ public void extension_plugin_is_ignored_if_base_plugin_is_missing_at_startup() throws Exception {
+ copyTestPluginTo("test-extend-plugin", fs.getInstalledPluginsDir());
+
+ underTest.start();
+
+ // plugin is not installed as its base plugin is not installed
+ assertThat(underTest.getPluginInfos()).isEmpty();
+ }
+
+ @Test
+ public void fail_to_get_missing_plugins() {
+ underTest.start();
+ try {
+ underTest.getPluginInfo("unknown");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessage("Plugin [unknown] does not exist");
+ }
+
+ try {
+ underTest.getPluginInstance("unknown");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessage("Plugin [unknown] does not exist");
+ }
+ }
+
+ @Test
+ public void plugin_is_incompatible_if_no_entry_point_class() {
+ PluginInfo plugin = new PluginInfo("foo").setName("Foo");
+ assertThat(ServerPluginRepository.isCompatible(plugin, runtime, Collections.emptyMap())).isFalse();
+ assertThat(logs.logs()).contains("Plugin Foo [foo] is ignored because entry point class is not defined");
+ }
+
+ @Test
+ public void fail_when_views_is_installed() throws Exception {
+ copyTestPluginTo("fake-views-plugin", fs.getInstalledPluginsDir());
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Plugin 'views' is no longer compatible with this version of SonarQube");
+ underTest.start();
+ }
+
+ @Test
+ public void fail_when_sqale_plugin_is_installed() throws Exception {
+ copyTestPluginTo("fake-sqale-plugin", fs.getInstalledPluginsDir());
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Plugin 'sqale' is no longer compatible with this version of SonarQube");
+ underTest.start();
+ }
+
+ @Test
+ public void fail_when_report_is_installed() throws Exception {
+ copyTestPluginTo("fake-report-plugin", fs.getInstalledPluginsDir());
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Plugin 'report' is no longer compatible with this version of SonarQube");
+ underTest.start();
+ }
+
+ /**
+ * Some plugins can only extend the classloader of base plugin, without declaring new extensions.
+ */
+ @Test
+ public void plugin_is_compatible_if_no_entry_point_class_but_extend_other_plugin() {
+ PluginInfo basePlugin = new PluginInfo("base").setMainClass("org.bar.Bar");
+ PluginInfo plugin = new PluginInfo("foo").setBasePlugin("base");
+ Map<String, PluginInfo> plugins = ImmutableMap.of("base", basePlugin, "foo", plugin);
+
+ assertThat(ServerPluginRepository.isCompatible(plugin, runtime, plugins)).isTrue();
+ }
+
+ @Test
+ public void getPluginInstance_throws_ISE_if_repo_is_not_started() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("not started yet");
+
+ underTest.getPluginInstance("foo");
+ }
+
+ @Test
+ public void getPluginInfo_throws_ISE_if_repo_is_not_started() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("not started yet");
+
+ underTest.getPluginInfo("foo");
+ }
+
+ @Test
+ public void hasPlugin_throws_ISE_if_repo_is_not_started() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("not started yet");
+
+ underTest.hasPlugin("foo");
+ }
+
+ @Test
+ public void getPluginInfos_throws_ISE_if_repo_is_not_started() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("not started yet");
+
+ underTest.getPluginInfos();
+ }
+
+ private File copyTestPluginTo(String testPluginName, File toDir) throws IOException {
+ File jar = TestProjectUtils.jarOf(testPluginName);
+ // file is copied because it's supposed to be moved by the test
+ FileUtils.copyFileToDirectory(jar, toDir);
+ return new File(toDir, jar.getName());
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestPluginA.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestPluginA.java
new file mode 100644
index 00000000000..7952eb5799c
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestPluginA.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import org.sonar.api.Plugin;
+
+public class TestPluginA implements Plugin {
+ @Override
+ public void define(Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestProjectUtils.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestProjectUtils.java
new file mode 100644
index 00000000000..9710a8689e4
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/TestProjectUtils.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.util.Collection;
+
+public class TestProjectUtils {
+
+ /**
+ * Get the artifact of plugins stored in src/test/projects
+ */
+ public static File jarOf(String dirName) {
+ File target = FileUtils.toFile(TestProjectUtils.class.getResource(String.format("/%s/target/", dirName)));
+ Collection<File> jars = FileUtils.listFiles(target, new String[] {"jar"}, false);
+ if (jars == null || jars.size() != 1) {
+ throw new IllegalArgumentException("Test project is badly defined: " + dirName);
+ }
+ return jars.iterator().next();
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java
new file mode 100644
index 00000000000..b1e50cba77c
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterClientTest.java
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.SonarException;
+import org.sonar.api.utils.UriReader;
+import org.sonar.process.ProcessProperties;
+import org.sonar.updatecenter.common.UpdateCenter;
+import org.sonar.updatecenter.common.Version;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class UpdateCenterClientTest {
+
+ private static final String BASE_URL = "https://update.sonarsource.org";
+ private UriReader reader = mock(UriReader.class);
+ private MapSettings settings = new MapSettings();
+ private UpdateCenterClient underTest;
+
+ @Before
+ public void startServer() throws Exception {
+ reader = mock(UriReader.class);
+ settings.setProperty(UpdateCenterClient.URL_PROPERTY, BASE_URL);
+ settings.setProperty(ProcessProperties.Property.SONAR_UPDATECENTER_ACTIVATE.getKey(), true);
+ underTest = new UpdateCenterClient(reader, settings.asConfig());
+ }
+
+ @Test
+ public void downloadUpdateCenter() throws URISyntaxException {
+ when(reader.readString(new URI(BASE_URL), StandardCharsets.UTF_8)).thenReturn("publicVersions=2.2,2.3");
+ UpdateCenter plugins = underTest.getUpdateCenter().get();
+ verify(reader, times(1)).readString(new URI(BASE_URL), StandardCharsets.UTF_8);
+ assertThat(plugins.getSonar().getVersions()).containsOnly(Version.create("2.2"), Version.create("2.3"));
+ assertThat(underTest.getLastRefreshDate()).isNotNull();
+ }
+
+ @Test
+ public void not_available_before_initialization() {
+ assertThat(underTest.getLastRefreshDate()).isNull();
+ }
+
+ @Test
+ public void ignore_connection_errors() {
+ when(reader.readString(any(URI.class), eq(StandardCharsets.UTF_8))).thenThrow(new SonarException());
+ assertThat(underTest.getUpdateCenter()).isAbsent();
+ }
+
+ @Test
+ public void cache_data() throws Exception {
+ when(reader.readString(new URI(BASE_URL), StandardCharsets.UTF_8)).thenReturn("sonar.versions=2.2,2.3");
+
+ underTest.getUpdateCenter();
+ underTest.getUpdateCenter();
+
+ verify(reader, times(1)).readString(new URI(BASE_URL), StandardCharsets.UTF_8);
+ }
+
+ @Test
+ public void forceRefresh() throws Exception {
+ when(reader.readString(new URI(BASE_URL), StandardCharsets.UTF_8)).thenReturn("sonar.versions=2.2,2.3");
+
+ underTest.getUpdateCenter();
+ underTest.getUpdateCenter(true);
+
+ verify(reader, times(2)).readString(new URI(BASE_URL), StandardCharsets.UTF_8);
+ }
+
+ @Test
+ public void update_center_is_null_when_property_is_false() {
+ settings.setProperty(ProcessProperties.Property.SONAR_UPDATECENTER_ACTIVATE.getKey(), false);
+
+ assertThat(underTest.getUpdateCenter()).isAbsent();
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java
new file mode 100644
index 00000000000..a71522a77d5
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterMatrixFactoryTest.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.base.Optional;
+import org.junit.Test;
+import org.sonar.api.SonarRuntime;
+import org.sonar.updatecenter.common.UpdateCenter;
+
+import static org.assertj.guava.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class UpdateCenterMatrixFactoryTest {
+
+ private UpdateCenterMatrixFactory underTest;
+
+ @Test
+ public void return_absent_update_center() {
+ UpdateCenterClient updateCenterClient = mock(UpdateCenterClient.class);
+ when(updateCenterClient.getUpdateCenter(anyBoolean())).thenReturn(Optional.absent());
+
+ underTest = new UpdateCenterMatrixFactory(updateCenterClient, mock(SonarRuntime.class), mock(InstalledPluginReferentialFactory.class));
+
+ Optional<UpdateCenter> updateCenter = underTest.getUpdateCenter(false);
+
+ assertThat(updateCenter).isAbsent();
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterServlet.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterServlet.java
new file mode 100644
index 00000000000..d6d9904452d
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/UpdateCenterServlet.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import javax.servlet.GenericServlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.Properties;
+
+public class UpdateCenterServlet extends GenericServlet {
+
+ int count = 0;
+
+ @Override
+ public void service(ServletRequest request, ServletResponse response) throws IOException {
+ count++;
+ Properties props = new Properties();
+ props.setProperty("count", String.valueOf(count));
+ props.setProperty("agent", ((HttpServletRequest)request).getHeader("User-Agent"));
+ props.store(response.getOutputStream(), null);
+ }
+}
+
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/edition/EditionBundledPluginsTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/edition/EditionBundledPluginsTest.java
new file mode 100644
index 00000000000..907768f56fa
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/plugins/edition/EditionBundledPluginsTest.java
@@ -0,0 +1,178 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins.edition;
+
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.updatecenter.common.Plugin;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EditionBundledPluginsTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private final Random random = new Random();
+
+ @Test
+ public void isEditionBundled_on_Plugin_fails_with_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+
+ EditionBundledPlugins.isEditionBundled((Plugin) null);
+ }
+
+ @Test
+ public void isEditionBundled_on_Plugin_returns_false_for_SonarSource_and_non_commercial_license() {
+ Plugin plugin = newPlugin(randomizeCase("SonarSource"), randomAlphanumeric(3));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(plugin)).isFalse();
+ }
+
+ @Test
+ public void isEditionBundled_on_Plugin_returns_false_for_license_SonarSource_and_non_SonarSource_organization() {
+ Plugin plugin = newPlugin(randomAlphanumeric(3), randomizeCase("SonarSource"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(plugin)).isFalse();
+ }
+
+ @Test
+ public void isEditionBundled_on_Plugin_returns_false_for_license_Commercial_and_non_SonarSource_organization() {
+ Plugin plugin = newPlugin(randomAlphanumeric(3), randomizeCase("Commercial"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(plugin)).isFalse();
+ }
+
+ @Test
+ public void isEditionBundled_on_Plugin_returns_true_for_organization_SonarSource_and_license_SonarSource_case_insensitive() {
+ Plugin plugin = newPlugin(randomizeCase("SonarSource"), randomizeCase("SonarSource"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(plugin)).isTrue();
+ }
+
+ @Test
+ public void isEditionBundled_on_Plugin_returns_true_for_organization_SonarSource_and_license_Commercial_case_insensitive() {
+ Plugin plugin = newPlugin(randomizeCase("SonarSource"), randomizeCase("Commercial"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(plugin)).isTrue();
+ }
+
+ @Test
+ public void isEditionBundled_on_PluginInfo_fails_with_NPE_if_arg_is_null() {
+ expectedException.expect(NullPointerException.class);
+
+ EditionBundledPlugins.isEditionBundled((PluginInfo) null);
+ }
+
+ @Test
+ public void isEditionBundled_on_PluginInfo_returns_false_for_SonarSource_and_non_commercial_license() {
+ PluginInfo pluginInfo = newPluginInfo(randomizeCase("SonarSource"), randomAlphanumeric(3));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(pluginInfo)).isFalse();
+ }
+
+ @Test
+ public void isEditionBundled_on_PluginInfo_returns_false_for_license_SonarSource_and_non_SonarSource_organization() {
+ PluginInfo pluginInfo = newPluginInfo(randomAlphanumeric(3), randomizeCase("SonarSource"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(pluginInfo)).isFalse();
+ }
+
+ @Test
+ public void isEditionBundled_on_PluginInfo_returns_false_for_license_Commercial_and_non_SonarSource_organization() {
+ PluginInfo pluginInfo = newPluginInfo(randomAlphanumeric(3), randomizeCase("Commercial"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(pluginInfo)).isFalse();
+ }
+
+ @Test
+ public void isEditionBundled_on_PluginInfo_returns_true_for_organization_SonarSource_and_license_SonarSource_case_insensitive() {
+ PluginInfo pluginInfo = newPluginInfo(randomizeCase("SonarSource"), randomizeCase("SonarSource"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(pluginInfo)).isTrue();
+ }
+
+ @Test
+ public void isEditionBundled_on_PluginINfo_returns_true_for_organization_SonarSource_and_license_Commercial_case_insensitive() {
+ PluginInfo pluginInfo = newPluginInfo(randomizeCase("SonarSource"), randomizeCase("Commercial"));
+
+ assertThat(EditionBundledPlugins.isEditionBundled(pluginInfo)).isTrue();
+ }
+
+ private String randomizeCase(String s) {
+ return s.chars()
+ .map(c -> random.nextBoolean() ? Character.toUpperCase(c) : Character.toLowerCase(c))
+ .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+ .toString();
+ }
+
+ private PluginInfo newPluginInfo(String organization, String license) {
+ PluginInfo pluginInfo = new PluginInfo(randomAlphanumeric(2));
+ if (random.nextBoolean()) {
+ pluginInfo.setName(randomAlphanumeric(3));
+ }
+ if (random.nextBoolean()) {
+ pluginInfo.setOrganizationUrl(randomAlphanumeric(4));
+ }
+ if (random.nextBoolean()) {
+ pluginInfo.setIssueTrackerUrl(randomAlphanumeric(5));
+ }
+ if (random.nextBoolean()) {
+ pluginInfo.setIssueTrackerUrl(randomAlphanumeric(6));
+ }
+ if (random.nextBoolean()) {
+ pluginInfo.setBasePlugin(randomAlphanumeric(7));
+ }
+ if (random.nextBoolean()) {
+ pluginInfo.setHomepageUrl(randomAlphanumeric(8));
+ }
+ return pluginInfo
+ .setOrganizationName(organization)
+ .setLicense(license);
+ }
+
+ private Plugin newPlugin(String organization, String license) {
+ Plugin plugin = Plugin.factory(randomAlphanumeric(2));
+ if (random.nextBoolean()) {
+ plugin.setName(randomAlphanumeric(3));
+ }
+ if (random.nextBoolean()) {
+ plugin.setOrganizationUrl(randomAlphanumeric(4));
+ }
+ if (random.nextBoolean()) {
+ plugin.setTermsConditionsUrl(randomAlphanumeric(5));
+ }
+ if (random.nextBoolean()) {
+ plugin.setIssueTrackerUrl(randomAlphanumeric(6));
+ }
+ if (random.nextBoolean()) {
+ plugin.setCategory(randomAlphanumeric(7));
+ }
+ if (random.nextBoolean()) {
+ plugin.setHomepageUrl(randomAlphanumeric(8));
+ }
+ return plugin
+ .setLicense(license)
+ .setOrganization(organization);
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java
new file mode 100644
index 00000000000..8085b643ede
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java
@@ -0,0 +1,311 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Collections;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.sonar.core.util.stream.MoreCollectors;
+
+import static java.util.Collections.singleton;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
+import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto;
+
+@RunWith(DataProviderRunner.class)
+public class ProjectLifeCycleListenersImplTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private ProjectLifeCycleListener listener1 = mock(ProjectLifeCycleListener.class);
+ private ProjectLifeCycleListener listener2 = mock(ProjectLifeCycleListener.class);
+ private ProjectLifeCycleListener listener3 = mock(ProjectLifeCycleListener.class);
+ private ProjectLifeCycleListenersImpl underTestNoListeners = new ProjectLifeCycleListenersImpl();
+ private ProjectLifeCycleListenersImpl underTestWithListeners = new ProjectLifeCycleListenersImpl(
+ new ProjectLifeCycleListener[] {listener1, listener2, listener3});
+
+ @Test
+ public void onProjectsDeleted_throws_NPE_if_set_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("projects can't be null");
+
+ underTestWithListeners.onProjectsDeleted(null);
+ }
+
+ @Test
+ public void onProjectsDeleted_throws_NPE_if_set_is_null_even_if_no_listeners() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("projects can't be null");
+
+ underTestNoListeners.onProjectsDeleted(null);
+ }
+
+ @Test
+ public void onProjectsDeleted_has_no_effect_if_set_is_empty() {
+ underTestNoListeners.onProjectsDeleted(Collections.emptySet());
+
+ underTestWithListeners.onProjectsDeleted(Collections.emptySet());
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectsDeleted_does_not_fail_if_there_is_no_listener(Set<Project> projects) {
+ underTestNoListeners.onProjectsDeleted(projects);
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectsDeleted_calls_all_listeners_in_order_of_addition_to_constructor(Set<Project> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+
+ underTestWithListeners.onProjectsDeleted(projects);
+
+ inOrder.verify(listener1).onProjectsDeleted(same(projects));
+ inOrder.verify(listener2).onProjectsDeleted(same(projects));
+ inOrder.verify(listener3).onProjectsDeleted(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectsDeleted_calls_all_listeners_even_if_one_throws_an_Exception(Set<Project> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+ doThrow(new RuntimeException("Faking listener2 throwing an exception"))
+ .when(listener2)
+ .onProjectsDeleted(any());
+
+ underTestWithListeners.onProjectsDeleted(projects);
+
+ inOrder.verify(listener1).onProjectsDeleted(same(projects));
+ inOrder.verify(listener2).onProjectsDeleted(same(projects));
+ inOrder.verify(listener3).onProjectsDeleted(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectsDeleted_calls_all_listeners_even_if_one_throws_an_Error(Set<Project> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+ doThrow(new Error("Faking listener2 throwing an Error"))
+ .when(listener2)
+ .onProjectsDeleted(any());
+
+ underTestWithListeners.onProjectsDeleted(projects);
+
+ inOrder.verify(listener1).onProjectsDeleted(same(projects));
+ inOrder.verify(listener2).onProjectsDeleted(same(projects));
+ inOrder.verify(listener3).onProjectsDeleted(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void onProjectBranchesDeleted_throws_NPE_if_set_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("projects can't be null");
+
+ underTestWithListeners.onProjectBranchesDeleted(null);
+ }
+
+ @Test
+ public void onProjectBranchesDeleted_throws_NPE_if_set_is_null_even_if_no_listeners() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("projects can't be null");
+
+ underTestNoListeners.onProjectBranchesDeleted(null);
+ }
+
+ @Test
+ public void onProjectBranchesDeleted_has_no_effect_if_set_is_empty() {
+ underTestNoListeners.onProjectBranchesDeleted(Collections.emptySet());
+
+ underTestWithListeners.onProjectBranchesDeleted(Collections.emptySet());
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectBranchesDeleted_does_not_fail_if_there_is_no_listener(Set<Project> projects) {
+ underTestNoListeners.onProjectBranchesDeleted(projects);
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectBranchesDeleted_calls_all_listeners_in_order_of_addition_to_constructor(Set<Project> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+
+ underTestWithListeners.onProjectBranchesDeleted(projects);
+
+ inOrder.verify(listener1).onProjectBranchesDeleted(same(projects));
+ inOrder.verify(listener2).onProjectBranchesDeleted(same(projects));
+ inOrder.verify(listener3).onProjectBranchesDeleted(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectBranchesDeleted_calls_all_listeners_even_if_one_throws_an_Exception(Set<Project> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+ doThrow(new RuntimeException("Faking listener2 throwing an exception"))
+ .when(listener2)
+ .onProjectBranchesDeleted(any());
+
+ underTestWithListeners.onProjectBranchesDeleted(projects);
+
+ inOrder.verify(listener1).onProjectBranchesDeleted(same(projects));
+ inOrder.verify(listener2).onProjectBranchesDeleted(same(projects));
+ inOrder.verify(listener3).onProjectBranchesDeleted(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyProjects")
+ public void onProjectBranchesDeleted_calls_all_listeners_even_if_one_throws_an_Error(Set<Project> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+ doThrow(new Error("Faking listener2 throwing an Error"))
+ .when(listener2)
+ .onProjectBranchesDeleted(any());
+
+ underTestWithListeners.onProjectBranchesDeleted(projects);
+
+ inOrder.verify(listener1).onProjectBranchesDeleted(same(projects));
+ inOrder.verify(listener2).onProjectBranchesDeleted(same(projects));
+ inOrder.verify(listener3).onProjectBranchesDeleted(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @DataProvider
+ public static Object[][] oneOrManyProjects() {
+ return new Object[][] {
+ {singleton(newUniqueProject())},
+ {IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> newUniqueProject()).collect(MoreCollectors.toSet())}
+ };
+ }
+ // SDSDS
+
+ @Test
+ public void onProjectsRekeyed_throws_NPE_if_set_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("rekeyedProjects can't be null");
+
+ underTestWithListeners.onProjectsRekeyed(null);
+ }
+
+ @Test
+ public void onProjectsRekeyed_throws_NPE_if_set_is_null_even_if_no_listeners() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("rekeyedProjects can't be null");
+
+ underTestNoListeners.onProjectsRekeyed(null);
+ }
+
+ @Test
+ public void onProjectsRekeyed_has_no_effect_if_set_is_empty() {
+ underTestNoListeners.onProjectsRekeyed(Collections.emptySet());
+
+ underTestWithListeners.onProjectsRekeyed(Collections.emptySet());
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyRekeyedProjects")
+ public void onProjectsRekeyed_does_not_fail_if_there_is_no_listener(Set<RekeyedProject> projects) {
+ underTestNoListeners.onProjectsRekeyed(projects);
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyRekeyedProjects")
+ public void onProjectsRekeyed_calls_all_listeners_in_order_of_addition_to_constructor(Set<RekeyedProject> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+
+ underTestWithListeners.onProjectsRekeyed(projects);
+
+ inOrder.verify(listener1).onProjectsRekeyed(same(projects));
+ inOrder.verify(listener2).onProjectsRekeyed(same(projects));
+ inOrder.verify(listener3).onProjectsRekeyed(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyRekeyedProjects")
+ public void onProjectsRekeyed_calls_all_listeners_even_if_one_throws_an_Exception(Set<RekeyedProject> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+ doThrow(new RuntimeException("Faking listener2 throwing an exception"))
+ .when(listener2)
+ .onProjectsRekeyed(any());
+
+ underTestWithListeners.onProjectsRekeyed(projects);
+
+ inOrder.verify(listener1).onProjectsRekeyed(same(projects));
+ inOrder.verify(listener2).onProjectsRekeyed(same(projects));
+ inOrder.verify(listener3).onProjectsRekeyed(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @UseDataProvider("oneOrManyRekeyedProjects")
+ public void onProjectsRekeyed_calls_all_listeners_even_if_one_throws_an_Error(Set<RekeyedProject> projects) {
+ InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+ doThrow(new Error("Faking listener2 throwing an Error"))
+ .when(listener2)
+ .onProjectsRekeyed(any());
+
+ underTestWithListeners.onProjectsRekeyed(projects);
+
+ inOrder.verify(listener1).onProjectsRekeyed(same(projects));
+ inOrder.verify(listener2).onProjectsRekeyed(same(projects));
+ inOrder.verify(listener3).onProjectsRekeyed(same(projects));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @DataProvider
+ public static Object[][] oneOrManyRekeyedProjects() {
+ return new Object[][] {
+ {singleton(newUniqueRekeyedProject())},
+ {IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> newUniqueRekeyedProject()).collect(MoreCollectors.toSet())}
+ };
+ }
+
+ private static Project newUniqueProject() {
+ return Project.from(newPrivateProjectDto(newOrganizationDto()));
+ }
+
+ private static int counter = 3_989;
+
+ private static RekeyedProject newUniqueRekeyedProject() {
+ int base = counter++;
+ Project project = Project.from(newPrivateProjectDto(newOrganizationDto()));
+ return new RekeyedProject(project, base + "_old_key");
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/project/RekeyedProjectTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/project/RekeyedProjectTest.java
new file mode 100644
index 00000000000..a847d91d0d8
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/project/RekeyedProjectTest.java
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.project;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.util.Collections.emptyList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
+import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto;
+
+public class RekeyedProjectTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void constructor_throws_NPE_if_project_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("project can't be null");
+
+ new RekeyedProject(null, randomAlphanumeric(3));
+ }
+
+ @Test
+ public void constructor_throws_NPE_if_previousKey_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("previousKey can't be null");
+
+ new RekeyedProject(newRandomProject(), null);
+ }
+
+ @Test
+ public void verify_getters() {
+ Project project = newRandomProject();
+ String previousKey = randomAlphanumeric(6);
+ RekeyedProject underTest = new RekeyedProject(project, previousKey);
+
+ assertThat(underTest.getProject()).isSameAs(project);
+ assertThat(underTest.getPreviousKey()).isEqualTo(previousKey);
+ }
+
+ @Test
+ public void equals_is_based_on_project_and_previousKey() {
+ Project project = newRandomProject();
+ String previousKey = randomAlphanumeric(6);
+ RekeyedProject underTest = new RekeyedProject(project, previousKey);
+
+ assertThat(underTest).isEqualTo(underTest);
+ assertThat(underTest).isEqualTo(new RekeyedProject(project, previousKey));
+ assertThat(underTest).isNotEqualTo(new RekeyedProject(project, randomAlphanumeric(11)));
+ assertThat(underTest).isNotEqualTo(new RekeyedProject(newRandomProject(), previousKey));
+ assertThat(underTest).isNotEqualTo(new Object());
+ assertThat(underTest).isNotEqualTo(null);
+ }
+
+ @Test
+ public void hashCode_is_based_on_project_and_previousKey() {
+ Project project = newRandomProject();
+ String previousKey = randomAlphanumeric(6);
+ RekeyedProject underTest = new RekeyedProject(project, previousKey);
+
+ assertThat(underTest.hashCode()).isEqualTo(underTest.hashCode());
+ assertThat(underTest.hashCode()).isEqualTo(new RekeyedProject(project, previousKey).hashCode());
+ assertThat(underTest.hashCode()).isNotEqualTo(new RekeyedProject(project, randomAlphanumeric(11)).hashCode());
+ assertThat(underTest.hashCode()).isNotEqualTo(new RekeyedProject(newRandomProject(), previousKey).hashCode());
+ assertThat(underTest.hashCode()).isNotEqualTo(new Object().hashCode());
+ assertThat(underTest.hashCode()).isNotEqualTo(null);
+ }
+
+ @Test
+ public void verify_toString() {
+ Project project = new Project("A", "B", "C", "D", emptyList());
+ String previousKey = "E";
+ RekeyedProject underTest = new RekeyedProject(project, previousKey);
+
+ assertThat(underTest.toString()).isEqualTo("RekeyedProject{project=Project{uuid='A', key='B', name='C', description='D'}, previousKey='E'}");
+ }
+
+ private static Project newRandomProject() {
+ return Project.from(newPrivateProjectDto(newOrganizationDto()));
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
new file mode 100644
index 00000000000..964637487fb
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
@@ -0,0 +1,358 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.commons.lang.RandomStringUtils;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.qualitygate.changeevent.QGChangeEventListener.ChangedIssue;
+import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl.ChangedIssueImpl;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class QGChangeEventListenersImplTest {
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private QGChangeEventListener listener1 = mock(QGChangeEventListener.class);
+ private QGChangeEventListener listener2 = mock(QGChangeEventListener.class);
+ private QGChangeEventListener listener3 = mock(QGChangeEventListener.class);
+ private List<QGChangeEventListener> listeners = Arrays.asList(listener1, listener2, listener3);
+
+ private String component1Uuid = RandomStringUtils.randomAlphabetic(6);
+ private ComponentDto component1 = newComponentDto(component1Uuid);
+ private DefaultIssue component1Issue = newDefaultIssue(component1Uuid);
+ private List<DefaultIssue> oneIssueOnComponent1 = singletonList(component1Issue);
+ private QGChangeEvent component1QGChangeEvent = newQGChangeEvent(component1);
+
+ private InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+
+ private QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener1, listener2, listener3});
+
+ @Test
+ public void broadcastOnIssueChange_has_no_effect_when_issues_are_empty() {
+ underTest.broadcastOnIssueChange(emptyList(), singletonList(component1QGChangeEvent));
+
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcastOnIssueChange_has_no_effect_when_no_changeEvent() {
+ underTest.broadcastOnIssueChange(oneIssueOnComponent1, emptySet());
+
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcastOnIssueChange_passes_same_arguments_to_all_listeners_in_order_of_addition_to_constructor() {
+ underTest.broadcastOnIssueChange(oneIssueOnComponent1, singletonList(component1QGChangeEvent));
+
+ ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
+ inOrder.verify(listener1).onIssueChanges(same(component1QGChangeEvent), changedIssuesCaptor.capture());
+ Set<ChangedIssue> changedIssues = changedIssuesCaptor.getValue();
+ inOrder.verify(listener2).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verify(listener3).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void broadcastOnIssueChange_calls_all_listeners_even_if_one_throws_an_exception() {
+ QGChangeEventListener failingListener = new QGChangeEventListener[] {listener1, listener2, listener3}[new Random().nextInt(3)];
+ doThrow(new RuntimeException("Faking an exception thrown by onChanges"))
+ .when(failingListener)
+ .onIssueChanges(any(), any());
+
+ underTest.broadcastOnIssueChange(oneIssueOnComponent1, singletonList(component1QGChangeEvent));
+
+ ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
+ inOrder.verify(listener1).onIssueChanges(same(component1QGChangeEvent), changedIssuesCaptor.capture());
+ Set<ChangedIssue> changedIssues = changedIssuesCaptor.getValue();
+ inOrder.verify(listener2).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verify(listener3).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verifyNoMoreInteractions();
+ assertThat(logTester.logs()).hasSize(4);
+ assertThat(logTester.logs(LoggerLevel.WARN)).hasSize(1);
+ }
+
+ @Test
+ public void broadcastOnIssueChange_stops_calling_listeners_when_one_throws_an_ERROR() {
+ doThrow(new Error("Faking an error thrown by a listener"))
+ .when(listener2)
+ .onIssueChanges(any(), any());
+
+ underTest.broadcastOnIssueChange(oneIssueOnComponent1, singletonList(component1QGChangeEvent));
+
+ ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
+ inOrder.verify(listener1).onIssueChanges(same(component1QGChangeEvent), changedIssuesCaptor.capture());
+ Set<ChangedIssue> changedIssues = changedIssuesCaptor.getValue();
+ inOrder.verify(listener2).onIssueChanges(same(component1QGChangeEvent), same(changedIssues));
+ inOrder.verifyNoMoreInteractions();
+ assertThat(logTester.logs()).hasSize(3);
+ assertThat(logTester.logs(LoggerLevel.WARN)).hasSize(1);
+ }
+
+ @Test
+ public void broadcastOnIssueChange_logs_each_listener_call_at_TRACE_level() {
+ underTest.broadcastOnIssueChange(oneIssueOnComponent1, singletonList(component1QGChangeEvent));
+
+ assertThat(logTester.logs()).hasSize(3);
+ List<String> traceLogs = logTester.logs(LoggerLevel.TRACE);
+ assertThat(traceLogs).hasSize(3)
+ .containsOnly(
+ "calling onChange() on listener " + listener1.getClass().getName() + " for events " + component1QGChangeEvent.toString() + "...",
+ "calling onChange() on listener " + listener2.getClass().getName() + " for events " + component1QGChangeEvent.toString() + "...",
+ "calling onChange() on listener " + listener3.getClass().getName() + " for events " + component1QGChangeEvent.toString() + "...");
+ }
+
+ @Test
+ public void broadcastOnIssueChange_passes_immutable_set_of_ChangedIssues() {
+ QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener1});
+
+ underTest.broadcastOnIssueChange(oneIssueOnComponent1, singletonList(component1QGChangeEvent));
+
+ ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
+ inOrder.verify(listener1).onIssueChanges(same(component1QGChangeEvent), changedIssuesCaptor.capture());
+ assertThat(changedIssuesCaptor.getValue()).isInstanceOf(ImmutableSet.class);
+ }
+
+ @Test
+ public void broadcastOnIssueChange_has_no_effect_when_no_listener() {
+ QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl();
+
+ underTest.broadcastOnIssueChange(oneIssueOnComponent1, singletonList(component1QGChangeEvent));
+
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcastOnIssueChange_calls_listener_for_each_component_uuid_with_at_least_one_QGChangeEvent() {
+ // component2 has multiple issues
+ ComponentDto component2 = newComponentDto(component1Uuid + "2");
+ DefaultIssue[] component2Issues = {newDefaultIssue(component2.uuid()), newDefaultIssue(component2.uuid())};
+ QGChangeEvent component2QGChangeEvent = newQGChangeEvent(component2);
+
+ // component 3 has multiple QGChangeEvent and only one issue
+ ComponentDto component3 = newComponentDto(component1Uuid + "3");
+ DefaultIssue component3Issue = newDefaultIssue(component3.uuid());
+ QGChangeEvent[] component3QGChangeEvents = {newQGChangeEvent(component3), newQGChangeEvent(component3)};
+
+ // component 4 has multiple QGChangeEvent and multiples issues
+ ComponentDto component4 = newComponentDto(component1Uuid + "4");
+ DefaultIssue[] component4Issues = {newDefaultIssue(component4.uuid()), newDefaultIssue(component4.uuid())};
+ QGChangeEvent[] component4QGChangeEvents = {newQGChangeEvent(component4), newQGChangeEvent(component4)};
+
+ // component 5 has no QGChangeEvent but one issue
+ ComponentDto component5 = newComponentDto(component1Uuid + "5");
+ DefaultIssue component5Issue = newDefaultIssue(component5.uuid());
+
+ List<DefaultIssue> issues = Stream.of(
+ Stream.of(component1Issue),
+ Arrays.stream(component2Issues),
+ Stream.of(component3Issue),
+ Arrays.stream(component4Issues),
+ Stream.of(component5Issue))
+ .flatMap(s -> s)
+ .collect(Collectors.toList());
+
+ List<DefaultIssue> changedIssues = randomizedList(issues);
+ List<QGChangeEvent> qgChangeEvents = Stream.of(
+ Stream.of(component1QGChangeEvent),
+ Stream.of(component2QGChangeEvent),
+ Arrays.stream(component3QGChangeEvents),
+ Arrays.stream(component4QGChangeEvents))
+ .flatMap(s -> s)
+ .collect(Collectors.toList());
+
+ underTest.broadcastOnIssueChange(changedIssues, randomizedList(qgChangeEvents));
+
+ listeners.forEach(listener -> {
+ verifyListenerCalled(listener, component1QGChangeEvent, component1Issue);
+ verifyListenerCalled(listener, component2QGChangeEvent, component2Issues);
+ Arrays.stream(component3QGChangeEvents)
+ .forEach(component3QGChangeEvent -> verifyListenerCalled(listener, component3QGChangeEvent, component3Issue));
+ Arrays.stream(component4QGChangeEvents)
+ .forEach(component4QGChangeEvent -> verifyListenerCalled(listener, component4QGChangeEvent, component4Issues));
+ });
+ verifyNoMoreInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void isNotClosed_returns_true_if_issue_in_one_of_opened_states() {
+ DefaultIssue defaultIssue = new DefaultIssue();
+ defaultIssue.setStatus(Issue.STATUS_REOPENED);
+ defaultIssue.setKey("abc");
+ defaultIssue.setType(RuleType.BUG);
+ defaultIssue.setSeverity("BLOCKER");
+
+ ChangedIssue changedIssue = new ChangedIssueImpl(defaultIssue);
+
+ assertThat(changedIssue.isNotClosed()).isTrue();
+ }
+
+ @Test
+ public void isNotClosed_returns_false_if_issue_in_one_of_closed_states() {
+ DefaultIssue defaultIssue = new DefaultIssue();
+ defaultIssue.setStatus(Issue.STATUS_CONFIRMED);
+ defaultIssue.setKey("abc");
+ defaultIssue.setType(RuleType.BUG);
+ defaultIssue.setSeverity("BLOCKER");
+
+ ChangedIssue changedIssue = new ChangedIssueImpl(defaultIssue);
+
+ assertThat(changedIssue.isNotClosed()).isFalse();
+ }
+
+ @Test
+ public void test_status_mapping() {
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_OPEN))).isEqualTo(QGChangeEventListener.Status.OPEN);
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_REOPENED))).isEqualTo(QGChangeEventListener.Status.REOPENED);
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_CONFIRMED))).isEqualTo(QGChangeEventListener.Status.CONFIRMED);
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FALSE_POSITIVE)))
+ .isEqualTo(QGChangeEventListener.Status.RESOLVED_FP);
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_WONT_FIX)))
+ .isEqualTo(QGChangeEventListener.Status.RESOLVED_WF);
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FIXED)))
+ .isEqualTo(QGChangeEventListener.Status.RESOLVED_FIXED);
+ try {
+ ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_CLOSED));
+ fail("Expected exception");
+ } catch (Exception e) {
+ assertThat(e).hasMessage("Unexpected status: CLOSED");
+ }
+ try {
+ ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED));
+ fail("Expected exception");
+ } catch (Exception e) {
+ assertThat(e).hasMessage("A resolved issue should have a resolution");
+ }
+ try {
+ ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_REMOVED));
+ fail("Expected exception");
+ } catch (Exception e) {
+ assertThat(e).hasMessage("Unexpected resolution for a resolved issue: REMOVED");
+ }
+ }
+
+ @Test
+ public void test_status_mapping_on_security_hotspots() {
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)))
+ .isEqualTo(QGChangeEventListener.Status.TO_REVIEW);
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_IN_REVIEW)))
+ .isEqualTo(QGChangeEventListener.Status.IN_REVIEW);
+ assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED)))
+ .isEqualTo(QGChangeEventListener.Status.REVIEWED);
+ }
+
+ private void verifyListenerCalled(QGChangeEventListener listener, QGChangeEvent changeEvent, DefaultIssue... issues) {
+ ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
+ verify(listener).onIssueChanges(same(changeEvent), changedIssuesCaptor.capture());
+ Set<ChangedIssue> changedIssues = changedIssuesCaptor.getValue();
+ Tuple[] expected = Arrays.stream(issues)
+ .map(issue -> tuple(issue.key(), ChangedIssueImpl.statusOf(issue), issue.type()))
+ .toArray(Tuple[]::new);
+ assertThat(changedIssues)
+ .hasSize(issues.length)
+ .extracting(ChangedIssue::getKey, ChangedIssue::getStatus, ChangedIssue::getType)
+ .containsOnly(expected);
+ }
+
+ private static final String[] POSSIBLE_STATUSES = asList(Issue.STATUS_CONFIRMED, Issue.STATUS_REOPENED, Issue.STATUS_RESOLVED).stream().toArray(String[]::new);
+ private static int issueIdCounter = 0;
+
+ private static DefaultIssue newDefaultIssue(String projectUuid) {
+ DefaultIssue defaultIssue = new DefaultIssue();
+ defaultIssue.setKey("issue_" + issueIdCounter++);
+ defaultIssue.setProjectUuid(projectUuid);
+ defaultIssue.setType(RuleType.values()[new Random().nextInt(RuleType.values().length)]);
+ defaultIssue.setStatus(POSSIBLE_STATUSES[new Random().nextInt(POSSIBLE_STATUSES.length)]);
+ String[] possibleResolutions = possibleResolutions(defaultIssue.getStatus());
+ if (possibleResolutions.length > 0) {
+ defaultIssue.setResolution(possibleResolutions[new Random().nextInt(possibleResolutions.length)]);
+ }
+ return defaultIssue;
+ }
+
+ private static String[] possibleResolutions(String status) {
+ switch (status) {
+ case Issue.STATUS_RESOLVED:
+ return new String[] {Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_WONT_FIX};
+ default:
+ return new String[0];
+ }
+ }
+
+ private static ComponentDto newComponentDto(String uuid) {
+ ComponentDto componentDto = new ComponentDto();
+ componentDto.setUuid(uuid);
+ return componentDto;
+ }
+
+ private static QGChangeEvent newQGChangeEvent(ComponentDto componentDto) {
+ QGChangeEvent res = mock(QGChangeEvent.class);
+ when(res.getProject()).thenReturn(componentDto);
+ return res;
+ }
+
+ private static <T> ArgumentCaptor<Set<T>> newSetCaptor() {
+ Class<Set<T>> clazz = (Class<Set<T>>) (Class) Set.class;
+ return ArgumentCaptor.forClass(clazz);
+ }
+
+ private static <T> List<T> randomizedList(List<T> issues) {
+ ArrayList<T> res = new ArrayList<>(issues);
+ Collections.shuffle(res);
+ return ImmutableList.copyOf(res);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java
new file mode 100644
index 00000000000..eb4ce5d6c60
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.qualitygate.changeevent;
+
+import java.util.Optional;
+import java.util.Random;
+import java.util.function.Supplier;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class QGChangeEventTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private ComponentDto project = new ComponentDto()
+ .setDbKey("foo")
+ .setUuid("bar");
+ private BranchDto branch = new BranchDto()
+ .setBranchType(BranchType.SHORT)
+ .setUuid("bar")
+ .setProjectUuid("doh")
+ .setMergeBranchUuid("zop");
+ private SnapshotDto analysis = new SnapshotDto()
+ .setUuid("pto")
+ .setCreatedAt(8_999_999_765L);
+ private Configuration configuration = Mockito.mock(Configuration.class);
+ private Metric.Level previousStatus = Metric.Level.values()[new Random().nextInt(Metric.Level.values().length)];
+ private Supplier<Optional<EvaluatedQualityGate>> supplier = Optional::empty;
+
+ @Test
+ public void constructor_fails_with_NPE_if_project_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("project can't be null");
+
+ new QGChangeEvent(null, branch, analysis, configuration, previousStatus, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_branch_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("branch can't be null");
+
+ new QGChangeEvent(project, null, analysis, configuration, previousStatus, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_analysis_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("analysis can't be null");
+
+ new QGChangeEvent(project, branch, null, configuration, previousStatus, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_configuration_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("projectConfiguration can't be null");
+
+ new QGChangeEvent(project, branch, analysis, null, previousStatus, supplier);
+ }
+
+ @Test
+ public void constructor_does_not_fail_with_NPE_if_previousStatus_is_null() {
+ new QGChangeEvent(project, branch, analysis, configuration, null, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_supplier_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("qualityGateSupplier can't be null");
+
+ new QGChangeEvent(project, branch, analysis, configuration, previousStatus, null);
+ }
+
+ @Test
+ public void verify_getters() {
+ QGChangeEvent underTest = new QGChangeEvent(project, branch, analysis, configuration, previousStatus, supplier);
+
+ assertThat(underTest.getProject()).isSameAs(project);
+ assertThat(underTest.getBranch()).isSameAs(branch);
+ assertThat(underTest.getAnalysis()).isSameAs(analysis);
+ assertThat(underTest.getProjectConfiguration()).isSameAs(configuration);
+ assertThat(underTest.getPreviousStatus()).contains(previousStatus);
+ assertThat(underTest.getQualityGateSupplier()).isSameAs(supplier);
+ }
+
+ @Test
+ public void getPreviousStatus_returns_empty_when_previousStatus_is_null() {
+ QGChangeEvent underTest = new QGChangeEvent(project, branch, analysis, configuration, previousStatus, supplier);
+
+ assertThat(underTest.getPreviousStatus()).contains(previousStatus);
+ }
+
+ @Test
+ public void overrides_toString() {
+ QGChangeEvent underTest = new QGChangeEvent(project, branch, analysis, configuration, previousStatus, supplier);
+
+ assertThat(underTest.toString())
+ .isEqualTo("QGChangeEvent{project=bar:foo, branch=SHORT:bar:doh:zop, analysis=pto:8999999765" +
+ ", projectConfiguration=" + configuration.toString() +
+ ", previousStatus=" + previousStatus +
+ ", qualityGateSupplier=" + supplier + "}");
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/ProjectConfigurationLoaderImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/ProjectConfigurationLoaderImplTest.java
new file mode 100644
index 00000000000..0fb88ee9b05
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/ProjectConfigurationLoaderImplTest.java
@@ -0,0 +1,178 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.property.PropertiesDao;
+import org.sonar.db.property.PropertyDto;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class ProjectConfigurationLoaderImplTest {
+ private DbClient dbClient = mock(DbClient.class);
+ private DbSession dbSession = mock(DbSession.class);
+ private PropertiesDao propertiesDao = mock(PropertiesDao.class);
+ private MapSettings globalSettings = new MapSettings();
+ private ProjectConfigurationLoaderImpl underTest = new ProjectConfigurationLoaderImpl(globalSettings, dbClient);
+
+ @Before
+ public void setUp() throws Exception {
+ when(dbClient.openSession(anyBoolean()))
+ .thenThrow(new IllegalStateException("ProjectConfigurationLoaderImpl should not open DB session"));
+ when(dbClient.propertiesDao()).thenReturn(propertiesDao);
+ }
+
+ @Test
+ public void returns_empty_map_when_no_component() {
+ assertThat(underTest.loadProjectConfigurations(dbSession, Collections.emptySet()))
+ .isEmpty();
+
+ verifyZeroInteractions(propertiesDao);
+ }
+
+ @Test
+ public void return_configuration_with_just_global_settings_when_no_component_settings() {
+ String key = randomAlphanumeric(3);
+ String value = randomAlphanumeric(4);
+ String componentDbKey = randomAlphanumeric(5);
+ String componentUuid = randomAlphanumeric(6);
+ globalSettings.setProperty(key, value);
+ when(propertiesDao.selectProjectProperties(dbSession, componentDbKey))
+ .thenReturn(emptyList());
+ ComponentDto component = newComponentDto(componentDbKey, componentUuid);
+
+ Map<String, Configuration> configurations = underTest.loadProjectConfigurations(dbSession, singleton(component));
+
+ assertThat(configurations)
+ .containsOnlyKeys(componentUuid);
+ assertThat(configurations.get(componentUuid).get(key)).contains(value);
+ }
+
+ @Test
+ public void return_configuration_with_global_settings_and_component_settings() {
+ String globalKey = randomAlphanumeric(3);
+ String globalValue = randomAlphanumeric(4);
+ String componentDbKey = randomAlphanumeric(5);
+ String componentUuid = randomAlphanumeric(6);
+ String projectPropKey1 = randomAlphanumeric(7);
+ String projectPropValue1 = randomAlphanumeric(8);
+ String projectPropKey2 = randomAlphanumeric(9);
+ String projectPropValue2 = randomAlphanumeric(10);
+ globalSettings.setProperty(globalKey, globalValue);
+ when(propertiesDao.selectProjectProperties(dbSession, componentDbKey))
+ .thenReturn(ImmutableList.of(newPropertyDto(projectPropKey1, projectPropValue1), newPropertyDto(projectPropKey2, projectPropValue2)));
+ ComponentDto component = newComponentDto(componentDbKey, componentUuid);
+
+ Map<String, Configuration> configurations = underTest.loadProjectConfigurations(dbSession, singleton(component));
+
+ assertThat(configurations)
+ .containsOnlyKeys(componentUuid);
+ assertThat(configurations.get(componentUuid).get(globalKey)).contains(globalValue);
+ assertThat(configurations.get(componentUuid).get(projectPropKey1)).contains(projectPropValue1);
+ assertThat(configurations.get(componentUuid).get(projectPropKey2)).contains(projectPropValue2);
+ }
+
+ @Test
+ public void return_configuration_with_global_settings_main_branch_settings_and_branch_settings() {
+ String globalKey = randomAlphanumeric(3);
+ String globalValue = randomAlphanumeric(4);
+ String mainBranchDbKey = randomAlphanumeric(5);
+ String branchDbKey = mainBranchDbKey + ComponentDto.BRANCH_KEY_SEPARATOR + randomAlphabetic(5);
+ String branchUuid = randomAlphanumeric(6);
+ String mainBranchPropKey = randomAlphanumeric(7);
+ String mainBranchPropValue = randomAlphanumeric(8);
+ String branchPropKey = randomAlphanumeric(9);
+ String branchPropValue = randomAlphanumeric(10);
+ globalSettings.setProperty(globalKey, globalValue);
+ when(propertiesDao.selectProjectProperties(dbSession, mainBranchDbKey))
+ .thenReturn(ImmutableList.of(newPropertyDto(mainBranchPropKey, mainBranchPropValue)));
+ when(propertiesDao.selectProjectProperties(dbSession, branchDbKey))
+ .thenReturn(ImmutableList.of(newPropertyDto(branchPropKey, branchPropValue)));
+ ComponentDto component = newComponentDto(branchDbKey, branchUuid);
+
+ Map<String, Configuration> configurations = underTest.loadProjectConfigurations(dbSession, singleton(component));
+
+ assertThat(configurations)
+ .containsOnlyKeys(branchUuid);
+ assertThat(configurations.get(branchUuid).get(globalKey)).contains(globalValue);
+ assertThat(configurations.get(branchUuid).get(mainBranchPropKey)).contains(mainBranchPropValue);
+ assertThat(configurations.get(branchUuid).get(branchPropKey)).contains(branchPropValue);
+ }
+
+ @Test
+ public void loads_configuration_of_any_given_component_only_once() {
+ String mainBranch1DbKey = randomAlphanumeric(4);
+ String mainBranch1Uuid = randomAlphanumeric(5);
+ String branch1DbKey = mainBranch1DbKey + ComponentDto.BRANCH_KEY_SEPARATOR + randomAlphabetic(5);
+ String branch1Uuid = randomAlphanumeric(6);
+ String branch2DbKey = mainBranch1DbKey + ComponentDto.BRANCH_KEY_SEPARATOR + randomAlphabetic(7);
+ String branch2Uuid = randomAlphanumeric(8);
+ String mainBranch2DbKey = randomAlphanumeric(14);
+ String mainBranch2Uuid = randomAlphanumeric(15);
+ String branch3DbKey = mainBranch2DbKey + ComponentDto.BRANCH_KEY_SEPARATOR + randomAlphabetic(5);
+ String branch3Uuid = randomAlphanumeric(16);
+
+ ComponentDto mainBranch1 = newComponentDto(mainBranch1DbKey, mainBranch1Uuid);
+ ComponentDto branch1 = newComponentDto(branch1DbKey, branch1Uuid);
+ ComponentDto branch2 = newComponentDto(branch2DbKey, branch2Uuid);
+ ComponentDto mainBranch2 = newComponentDto(mainBranch2DbKey, mainBranch2Uuid);
+ ComponentDto branch3 = newComponentDto(branch3DbKey, branch3Uuid);
+
+ underTest.loadProjectConfigurations(dbSession, ImmutableSet.of(mainBranch1, mainBranch2, branch1, branch2, branch3));
+
+ verify(propertiesDao, times(1)).selectProjectProperties(dbSession, mainBranch1DbKey);
+ verify(propertiesDao, times(1)).selectProjectProperties(dbSession, mainBranch2DbKey);
+ verify(propertiesDao, times(1)).selectProjectProperties(dbSession, branch1DbKey);
+ verify(propertiesDao, times(1)).selectProjectProperties(dbSession, branch2DbKey);
+ verify(propertiesDao, times(1)).selectProjectProperties(dbSession, branch3DbKey);
+ verifyNoMoreInteractions(propertiesDao);
+ }
+
+ private ComponentDto newComponentDto(String componentDbKey, String componentUuid) {
+ return new ComponentDto().setDbKey(componentDbKey).setUuid(componentUuid);
+ }
+
+ private PropertyDto newPropertyDto(String projectKey1, String projectValue1) {
+ return new PropertyDto()
+ .setKey(projectKey1)
+ .setValue(projectValue1);
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/SettingsChangeNotifierTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/SettingsChangeNotifierTest.java
new file mode 100644
index 00000000000..577eda83519
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/SettingsChangeNotifierTest.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting;
+
+import org.junit.Test;
+import org.sonar.api.config.GlobalPropertyChangeHandler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class SettingsChangeNotifierTest {
+ @Test
+ public void onGlobalPropertyChange() {
+ GlobalPropertyChangeHandler handler = mock(GlobalPropertyChangeHandler.class);
+ SettingsChangeNotifier notifier = new SettingsChangeNotifier(new GlobalPropertyChangeHandler[] {handler});
+
+ notifier.onGlobalPropertyChange("foo", "bar");
+
+ verify(handler).onChange(argThat(change -> change.getKey().equals("foo") && change.getNewValue().equals("bar")));
+ }
+
+ @Test
+ public void no_handlers() {
+ SettingsChangeNotifier notifier = new SettingsChangeNotifier();
+
+ assertThat(notifier.changeHandlers).isEmpty();
+
+ // does not fail
+ notifier.onGlobalPropertyChange("foo", "bar");
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/TestProjectConfigurationLoader.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/TestProjectConfigurationLoader.java
new file mode 100644
index 00000000000..e0c0b6d2854
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/setting/TestProjectConfigurationLoader.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.sonar.api.config.Configuration;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+
+public class TestProjectConfigurationLoader implements ProjectConfigurationLoader {
+
+ private final Configuration config;
+
+ public TestProjectConfigurationLoader(Configuration config) {
+ this.config = config;
+ }
+
+ @Override
+ public Map<String, Configuration> loadProjectConfigurations(DbSession dbSession, Set<ComponentDto> projects) {
+ Map<String, Configuration> map = new HashMap<>();
+ for (ComponentDto project : projects) {
+ map.put(project.uuid(), config);
+ }
+ return map;
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/BooleanTypeValidationTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/BooleanTypeValidationTest.java
new file mode 100644
index 00000000000..d11c03152b8
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/BooleanTypeValidationTest.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BooleanTypeValidationTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private BooleanTypeValidation underTest = new BooleanTypeValidation();
+
+ @Test
+ public void key() {
+ assertThat(underTest.key()).isEqualTo("BOOLEAN");
+ }
+
+ @Test
+ public void not_fail_on_valid_boolean() {
+ underTest.validate("true", null);
+ underTest.validate("True", null);
+ underTest.validate("false", null);
+ underTest.validate("FALSE", null);
+ }
+
+ @Test
+ public void fail_on_invalid_boolean() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value 'abc' must be one of \"true\" or \"false\".");
+
+ underTest.validate("abc", null);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/FloatTypeValidationTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/FloatTypeValidationTest.java
new file mode 100644
index 00000000000..b88c3fbb651
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/FloatTypeValidationTest.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FloatTypeValidationTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private FloatTypeValidation validation = new FloatTypeValidation();
+
+ @Test
+ public void key() {
+ assertThat(validation.key()).isEqualTo("FLOAT");
+ }
+
+ @Test
+ public void not_fail_on_valid_float() {
+ validation.validate("10.2", null);
+ validation.validate("10", null);
+ validation.validate("-10.3", null);
+ }
+
+ @Test
+ public void fail_on_invalid_float() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value 'abc' must be an floating point number.");
+
+ validation.validate("abc", null);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java
new file mode 100644
index 00000000000..8a069e1eae2
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.util.GlobalLockManager.DEFAULT_LOCK_DURATION_SECONDS;
+
+public class GlobalLockManagerTest {
+
+ private final System2 system2 = mock(System2.class);
+
+ @Rule
+ public final DbTester dbTester = DbTester.create(system2);
+
+ private final GlobalLockManager underTest = new GlobalLockManager(dbTester.getDbClient());
+
+ @Test
+ public void tryLock_succeeds_when_created_for_the_first_time() {
+ assertThat(underTest.tryLock("newName")).isTrue();
+ }
+
+ @Test
+ public void tryLock_fails_when_previous_lock_is_too_recent() {
+ String name = "newName";
+ assertThat(underTest.tryLock(name)).isTrue();
+ assertThat(underTest.tryLock(name)).isFalse();
+ }
+
+ @Test
+ public void tryLock_succeeds_when_previous_lock_is_old_enough() {
+ String name = "newName";
+ long firstLock = 0;
+ long longEnoughAfterFirstLock = firstLock + DEFAULT_LOCK_DURATION_SECONDS * 1000;
+ long notLongEnoughAfterFirstLock = longEnoughAfterFirstLock - 1;
+
+ when(system2.now()).thenReturn(firstLock);
+ assertThat(underTest.tryLock(name)).isTrue();
+
+ when(system2.now()).thenReturn(notLongEnoughAfterFirstLock);
+ assertThat(underTest.tryLock(name)).isFalse();
+
+ when(system2.now()).thenReturn(longEnoughAfterFirstLock);
+ assertThat(underTest.tryLock(name)).isTrue();
+ }
+
+ @Test
+ public void locks_with_different_name_are_independent() {
+ assertThat(underTest.tryLock("newName1")).isTrue();
+ assertThat(underTest.tryLock("newName2")).isTrue();
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/IntegerTypeValidationTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/IntegerTypeValidationTest.java
new file mode 100644
index 00000000000..e9b1953dd5a
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/IntegerTypeValidationTest.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IntegerTypeValidationTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private IntegerTypeValidation validation = new IntegerTypeValidation();
+
+ @Test
+ public void key() {
+ assertThat(validation.key()).isEqualTo("INTEGER");
+ }
+
+ @Test
+ public void not_fail_on_valid_integer() {
+ validation.validate("10", null);
+ validation.validate("-10", null);
+ }
+
+ @Test
+ public void fail_on_string() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value 'abc' must be an integer.");
+
+ validation.validate("abc", null);
+ }
+
+ @Test
+ public void fail_on_float() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value '10.1' must be an integer.");
+
+ validation.validate("10.1", null);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/LongTypeValidationTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/LongTypeValidationTest.java
new file mode 100644
index 00000000000..2f7d7e0946e
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/LongTypeValidationTest.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.PropertyType;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LongTypeValidationTest {
+
+ LongTypeValidation underTest = new LongTypeValidation();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void key_is_long_type_name() {
+ assertThat(underTest.key()).isEqualTo(PropertyType.LONG.name());
+ }
+
+ @Test
+ public void do_not_fail_with_long_values() {
+ underTest.validate("1984", null);
+ underTest.validate("-1984", null);
+ }
+
+ @Test
+ public void fail_when_float() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value '3.14' must be a long.");
+
+ underTest.validate("3.14", null);
+ }
+
+ @Test
+ public void fail_when_string() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value 'original string' must be a long.");
+
+ underTest.validate("original string", null);
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringListTypeValidationTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringListTypeValidationTest.java
new file mode 100644
index 00000000000..9f2630fd0de
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringListTypeValidationTest.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class StringListTypeValidationTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private StringListTypeValidation validation = new StringListTypeValidation();
+
+ @Test
+ public void key() {
+ assertThat(validation.key()).isEqualTo("SINGLE_SELECT_LIST");
+ }
+
+ @Test
+ public void not_fail_on_valid_option() {
+ validation.validate("a", newArrayList("a", "b", "c"));
+ validation.validate("a", null);
+ }
+
+ @Test
+ public void fail_on_invalid_option() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Value 'abc' must be one of : a, b, c.");
+
+ validation.validate("abc", newArrayList("a", "b", "c"));
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringTypeValidationTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringTypeValidationTest.java
new file mode 100644
index 00000000000..dde3afa3543
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/StringTypeValidationTest.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class StringTypeValidationTest {
+
+ StringTypeValidation validation;
+
+ @Before
+ public void setUp() {
+ validation = new StringTypeValidation();
+ }
+
+ @Test
+ public void key() {
+ assertThat(validation.key()).isEqualTo("STRING");
+ }
+
+ @Test
+ public void not_fail_on_valid_string() {
+ validation.validate("10", null);
+ validation.validate("abc", null);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TextTypeValidationTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TextTypeValidationTest.java
new file mode 100644
index 00000000000..4261f95c580
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TextTypeValidationTest.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TextTypeValidationTest {
+
+ TextTypeValidation validation;
+
+ @Before
+ public void setUp() {
+ validation = new TextTypeValidation();
+ }
+
+ @Test
+ public void key() {
+ assertThat(validation.key()).isEqualTo("TEXT");
+ }
+
+ @Test
+ public void not_fail_on_valid_text() {
+ validation.validate("10", null);
+ validation.validate("abc", null);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationModuleTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationModuleTest.java
new file mode 100644
index 00000000000..3ff709c8f0f
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationModuleTest.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TypeValidationModuleTest {
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new TypeValidationModule().configure(container);
+ assertThat(container.size()).isEqualTo(11);
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTest.java
new file mode 100644
index 00000000000..4dbae47cb37
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTest.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import org.junit.Test;
+import org.sonar.server.exceptions.BadRequestException;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TypeValidationsTest {
+
+ @Test
+ public void validate() {
+ TypeValidation fakeTypeValidation = mock(TypeValidation.class);
+ when(fakeTypeValidation.key()).thenReturn("Fake");
+
+ TypeValidations typeValidations = new TypeValidations(newArrayList(fakeTypeValidation));
+ typeValidations.validate("10", "Fake", newArrayList("a"));
+
+ verify(fakeTypeValidation).validate("10", newArrayList("a"));
+ }
+
+ @Test
+ public void validate__multiple_values() {
+ TypeValidation fakeTypeValidation = mock(TypeValidation.class);
+ when(fakeTypeValidation.key()).thenReturn("Fake");
+
+ TypeValidations typeValidations = new TypeValidations(newArrayList(fakeTypeValidation));
+ typeValidations.validate(newArrayList("10", "11", "12"), "Fake", newArrayList("11"));
+
+ verify(fakeTypeValidation).validate("10", newArrayList("11"));
+ }
+
+ @Test
+ public void fail_on_unknown_type() {
+ TypeValidation fakeTypeValidation = mock(TypeValidation.class);
+ when(fakeTypeValidation.key()).thenReturn("Fake");
+
+ try {
+ TypeValidations typeValidations = new TypeValidations(newArrayList(fakeTypeValidation));
+ typeValidations.validate("10", "Unknown", null);
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(BadRequestException.class);
+ BadRequestException badRequestException = (BadRequestException) e;
+ assertThat(badRequestException.getMessage()).isEqualTo("Type 'Unknown' is not valid.");
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTesting.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTesting.java
new file mode 100644
index 00000000000..b237ba62aab
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/TypeValidationsTesting.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.util;
+
+import java.util.Arrays;
+
+public class TypeValidationsTesting {
+ private TypeValidationsTesting() {
+ // utility class
+ }
+
+ public static TypeValidations newFullTypeValidations() {
+ return new TypeValidations(Arrays.asList(
+ new BooleanTypeValidation(),
+ new IntegerTypeValidation(),
+ new LongTypeValidation(),
+ new FloatTypeValidation(),
+ new StringTypeValidation(),
+ new StringListTypeValidation(),
+ new MetricLevelTypeValidation()
+ ));
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/.gitignore b/server/sonar-webserver-api/src/test/projects/.gitignore
new file mode 100644
index 00000000000..a945b8525e6
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/.gitignore
@@ -0,0 +1,7 @@
+# see README.txt
+!*/target/
+*/target/classes/
+*/target/maven-archiver/
+*/target/maven-status/
+*/target/test-*/
+
diff --git a/server/sonar-webserver-api/src/test/projects/README.txt b/server/sonar-webserver-api/src/test/projects/README.txt
new file mode 100644
index 00000000000..c53a66d52f2
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/README.txt
@@ -0,0 +1,3 @@
+This directory provides the fake plugins used by tests. These tests are rarely changed, so generated
+artifacts are stored in Git repository (see .gitignore). It avoids from adding unnecessary modules
+to build.
diff --git a/server/sonar-webserver-api/src/test/projects/fake-report-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/pom.xml
new file mode 100644
index 00000000000..72a04dbe04f
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>fake-report-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Fake Report Plugin</name>
+ <description>Fake Report Plugin</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>report</pluginKey>
+ <pluginClass>BasePlugin</pluginClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/BasePlugin.java b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/BasePlugin.java
new file mode 100644
index 00000000000..d12daff3e57
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/BasePlugin.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class BasePlugin extends Plugin {
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
new file mode 100644
index 00000000000..e0b54398eaf
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.plugins.testbase.api;
+
+public class BaseApi {
+ public void doNothing() {
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/fake-report-plugin/target/fake-report-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/target/fake-report-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..6085e44fdca
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-report-plugin/target/fake-report-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/pom.xml
new file mode 100644
index 00000000000..e417dd96fba
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>fake-sqale-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Fake SQALE Plugin</name>
+ <description>Fake SQALE Plugin</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>sqale</pluginKey>
+ <pluginClass>BasePlugin</pluginClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/BasePlugin.java b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/BasePlugin.java
new file mode 100644
index 00000000000..d12daff3e57
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/BasePlugin.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class BasePlugin extends Plugin {
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
new file mode 100644
index 00000000000..e0b54398eaf
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.plugins.testbase.api;
+
+public class BaseApi {
+ public void doNothing() {
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/target/fake-sqale-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/target/fake-sqale-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..b5c99f721b3
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-sqale-plugin/target/fake-sqale-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/fake-views-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/pom.xml
new file mode 100644
index 00000000000..1ef73d2ffda
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>fake-views-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Fake Views Plugin</name>
+ <description>Fake Views Plugin</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>views</pluginKey>
+ <pluginClass>BasePlugin</pluginClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/BasePlugin.java b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/BasePlugin.java
new file mode 100644
index 00000000000..d12daff3e57
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/BasePlugin.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class BasePlugin extends Plugin {
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
new file mode 100644
index 00000000000..e0b54398eaf
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.plugins.testbase.api;
+
+public class BaseApi {
+ public void doNothing() {
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/fake-views-plugin/target/fake-views-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/target/fake-views-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..a47d93d94a8
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/fake-views-plugin/target/fake-views-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/pom.xml b/server/sonar-webserver-api/src/test/projects/pom.xml
new file mode 100644
index 00000000000..37338313ac0
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/pom.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>parent</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <modules>
+ <module>test-base-plugin</module>
+ <module>test-base-plugin-v2</module>
+ <module>test-core-plugin</module>
+ <module>test-extend-plugin</module>
+ <module>test-libs-plugin</module>
+ <module>test-require-plugin</module>
+ <module>test-requirenew-plugin</module>
+ <module>fake-report-plugin</module>
+ <module>fake-sqale-plugin</module>
+ <module>fake-views-plugin</module>
+ </modules>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/pom.xml b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/pom.xml
new file mode 100644
index 00000000000..982be1c0170
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-base-plugin</artifactId>
+ <version>0.2-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Base Plugin</name>
+ <description>Simple standalone plugin. Used by other fake plugins.</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>testbase</pluginKey>
+ <pluginClass>BasePlugin</pluginClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/BasePlugin.java b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/BasePlugin.java
new file mode 100644
index 00000000000..d12daff3e57
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/BasePlugin.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class BasePlugin extends Plugin {
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/org/sonar/plugins/testbase/api/BaseApi.java b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/org/sonar/plugins/testbase/api/BaseApi.java
new file mode 100644
index 00000000000..e0b54398eaf
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/src/org/sonar/plugins/testbase/api/BaseApi.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.plugins.testbase.api;
+
+public class BaseApi {
+ public void doNothing() {
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/target/test-base-plugin-0.2-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/target/test-base-plugin-0.2-SNAPSHOT.jar
new file mode 100644
index 00000000000..1d4ef5430c7
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin-v2/target/test-base-plugin-0.2-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/test-base-plugin/pom.xml
new file mode 100644
index 00000000000..c4e95936e74
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-base-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Base Plugin</name>
+ <description>Simple standalone plugin. Used by other fake plugins.</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>testbase</pluginKey>
+ <pluginClass>BasePlugin</pluginClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin/src/BasePlugin.java b/server/sonar-webserver-api/src/test/projects/test-base-plugin/src/BasePlugin.java
new file mode 100644
index 00000000000..d12daff3e57
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin/src/BasePlugin.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class BasePlugin extends Plugin {
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java b/server/sonar-webserver-api/src/test/projects/test-base-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
new file mode 100644
index 00000000000..e0b54398eaf
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin/src/org/sonar/plugins/testbase/api/BaseApi.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.plugins.testbase.api;
+
+public class BaseApi {
+ public void doNothing() {
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-base-plugin/target/test-base-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/test-base-plugin/target/test-base-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..739a22fcdae
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-base-plugin/target/test-base-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/test-extend-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/test-extend-plugin/pom.xml
new file mode 100644
index 00000000000..e23667e6318
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-extend-plugin/pom.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-extend-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Test Extend Plugin</name>
+ <description>Fake plugin that extends the plugin with key "base"</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>testextend</pluginKey>
+ <pluginClass>ExtendPlugin</pluginClass>
+ <basePlugin>testbase</basePlugin>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/test-extend-plugin/src/ExtendPlugin.java b/server/sonar-webserver-api/src/test/projects/test-extend-plugin/src/ExtendPlugin.java
new file mode 100644
index 00000000000..d364a2f9dd4
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-extend-plugin/src/ExtendPlugin.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ExtendPlugin extends Plugin {
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-extend-plugin/target/test-extend-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/test-extend-plugin/target/test-extend-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..2f63c2e652c
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-extend-plugin/target/test-extend-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/test-libs-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/test-libs-plugin/pom.xml
new file mode 100644
index 00000000000..2d49cca2cf2
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-libs-plugin/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-libs-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Test Libs Plugin</name>
+ <description>Fake plugin that embeds some libraries</description>
+
+ <dependencies>
+ <!-- embedded libs. Chosen because small ! -->
+ <dependency>
+ <groupId>commons-email</groupId>
+ <artifactId>commons-email</artifactId>
+ <version>20030310.165926</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-daemon</groupId>
+ <artifactId>commons-daemon</artifactId>
+ <version>1.0.15</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>testlibs</pluginKey>
+ <pluginClass>LibsPlugin</pluginClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/test-libs-plugin/src/LibsPlugin.java b/server/sonar-webserver-api/src/test/projects/test-libs-plugin/src/LibsPlugin.java
new file mode 100644
index 00000000000..7e3ebe0909c
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-libs-plugin/src/LibsPlugin.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class LibsPlugin extends Plugin {
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-libs-plugin/target/test-libs-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/test-libs-plugin/target/test-libs-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..6ebe8652d8b
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-libs-plugin/target/test-libs-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/test-require-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/test-require-plugin/pom.xml
new file mode 100644
index 00000000000..62462ffbf34
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-require-plugin/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-require-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Test Require Plugin</name>
+ <description>This fake plugin depends on test-base-plugin</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-base-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <type>sonar-plugin</type>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>testrequire</pluginKey>
+ <pluginClass>RequirePlugin</pluginClass>
+ <requirePlugins>testbase:0.1</requirePlugins>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/test-require-plugin/src/RequirePlugin.java b/server/sonar-webserver-api/src/test/projects/test-require-plugin/src/RequirePlugin.java
new file mode 100644
index 00000000000..847ae2d994e
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-require-plugin/src/RequirePlugin.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class RequirePlugin extends Plugin {
+
+ public RequirePlugin() {
+ // call a class that is in the api published by the base plugin
+ new org.sonar.plugins.testbase.api.BaseApi().doNothing();
+ }
+
+ public void define(Plugin.Context context) {
+
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-require-plugin/target/test-require-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/test-require-plugin/target/test-require-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..f5fc95f9d0d
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-require-plugin/target/test-require-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/pom.xml b/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/pom.xml
new file mode 100644
index 00000000000..044cd94e8f0
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-requirenew-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>sonar-plugin</packaging>
+ <name>Test Require New Plugin</name>
+ <description>This fake plugin requires a version of test-base-plugin that is not installed</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-plugin-api</artifactId>
+ <version>4.5.4</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.sonarsource.sonarqube.tests</groupId>
+ <artifactId>test-base-plugin</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <type>sonar-plugin</type>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+ <artifactId>sonar-packaging-maven-plugin</artifactId>
+ <version>1.15</version>
+ <extensions>true</extensions>
+ <configuration>
+ <pluginKey>testrequire</pluginKey>
+ <pluginClass>RequirePlugin</pluginClass>
+ <requirePlugins>testbase:0.2</requirePlugins>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/src/RequirePlugin.java b/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/src/RequirePlugin.java
new file mode 100644
index 00000000000..0d14cde33c1
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/src/RequirePlugin.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+import org.sonar.api.Plugin;
+
+import java.util.Collections;
+import java.util.List;
+
+public class RequirePlugin extends Plugin {
+
+ public RequirePlugin() {
+ // call a class that is in the api published by the base plugin
+ new org.sonar.plugins.testbase.api.BaseApi().doNothing();
+ }
+
+ public void define(Plugin.Context context) {
+
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/target/test-requirenew-plugin-0.1-SNAPSHOT.jar b/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/target/test-requirenew-plugin-0.1-SNAPSHOT.jar
new file mode 100644
index 00000000000..0dd577fc360
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/projects/test-requirenew-plugin/target/test-requirenew-plugin-0.1-SNAPSHOT.jar
Binary files differ
diff --git a/server/sonar-webserver-api/src/test/resources/logback-test.xml b/server/sonar-webserver-api/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..3e34b0f9fc8
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/resources/logback-test.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>
+ %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+ <logger name="ch.qos.logback">
+ <level value="WARN"/>
+ </logger>
+
+ <logger name="okhttp3.mockwebserver">
+ <level value="WARN"/>
+ </logger>
+
+</configuration>