aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-core
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-08-14 11:07:17 +0200
committerSonarTech <sonartech@sonarsource.com>2019-08-14 20:21:16 +0200
commit98efbbadb8df0754a72292011e8ad3178abf90f2 (patch)
tree9b230b3f9b06078e085e98d486b7222f94f4269e /server/sonar-webserver-core
parentb4694fd3ae3ef3cd0e189883b490057447842441 (diff)
downloadsonarqube-98efbbadb8df0754a72292011e8ad3178abf90f2.tar.gz
sonarqube-98efbbadb8df0754a72292011e8ad3178abf90f2.zip
rename sonar-server to sonar-webserver-core
Diffstat (limited to 'server/sonar-webserver-core')
-rw-r--r--server/sonar-webserver-core/COPYING165
-rw-r--r--server/sonar-webserver-core/build.gradle81
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/app/WebServerProcessLogging.java57
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelPluginRepository.java140
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelXMLExporter.java204
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtRulesXMLImporter.java272
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/debt/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/es/IndexerStartupTask.java95
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/es/MigrationEsClientImpl.java77
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/es/package-info.java24
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationDaemon.java138
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationModule.java36
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/notification/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/AbstractSystemInfoWriter.java93
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/BackendCleanup.java249
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/ClusterVerification.java58
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DatabaseServerCompatibility.java73
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DefaultServerUpgradeStatus.java81
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/LogServerVersion.java64
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/PersistentSettings.java84
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/StartupMetadataPersister.java64
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java32
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartup.java56
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabase.java116
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabaseFactory.java66
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java52
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorService.java29
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceImpl.java44
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java116
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/BaseSectionMBean.java56
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSection.java125
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSectionMBean.java71
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsIndexesSection.java67
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java106
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/PluginsSection.java53
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SettingsSection.java78
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/StandaloneSystemSection.java138
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java31
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoader.java27
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImpl.java82
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSection.java65
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSection.java54
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoader.java45
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSection.java105
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeInfo.java99
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSection.java61
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoader.java30
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImpl.java67
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java34
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java69
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java163
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java35
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/AbortTomcatStartException.java45
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/RootFilter.java116
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/HttpRequestIdModule.java31
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdConfiguration.java35
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGenerator.java30
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBase.java29
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBaseImpl.java31
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImpl.java96
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdMDCStorage.java40
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CachingRuleFinder.java197
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitions.java26
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitionsImpl.java135
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java216
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/Rule.java110
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDefinitionsLoader.java64
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleParam.java37
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java154
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinder.java41
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinderImpl.java84
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/rule/package-info.java22
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java97
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/LogServerId.java44
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java139
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java128
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java104
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RenameDeprecatedPropertyKeys.java56
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/startup/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryClient.java92
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java175
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java131
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/package-info.java24
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/UpdateCenterModule.java33
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/util/ClassLoaderUtils.java114
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/util/DateCollector.java46
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/util/TempFolderCleaner.java53
-rw-r--r--server/sonar-webserver-core/src/main/java/org/sonar/server/util/package-info.java23
-rw-r--r--server/sonar-webserver-core/src/main/resources/build.properties1
-rw-r--r--server/sonar-webserver-core/src/main/resources/com/sonar/sqale/technical-debt-model.xml206
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java578
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelPluginRepositoryTest.java136
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelXMLExporterTest.java82
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtRulesXMLImporterTest.java240
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/es/IndexerStartupTaskTest.java93
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/es/MigrationEsClientImplTest.java125
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java1509
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java333
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java109
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java35
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationChannelTest.java44
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java105
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationMediumTest.java242
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationModuleTest.java35
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationTest.java77
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java369
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/BackendCleanupTest.java157
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClassLoaderUtilsTest.java76
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterVerificationTest.java85
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DatabaseServerCompatibilityTest.java104
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DefaultServerUpgradeStatusTest.java91
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/PersistentSettingsTest.java66
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StartupMetadataPersisterTest.java56
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java111
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartupTest.java68
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseFactoryTest.java71
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseTest.java168
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java136
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceAdaptor.java100
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplAsynchronousTest.java53
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java99
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java112
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/BaseSectionMBeanTest.java61
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/DbConnectionSectionTest.java70
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsIndexesSectionTest.java86
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java91
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSection.java40
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSectionMBean.java24
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/PluginsSectionTest.java62
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/SettingsSectionTest.java100
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/StandaloneSystemSectionTest.java155
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImplTest.java97
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSectionTest.java86
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSectionTest.java48
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoaderTest.java44
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSectionTest.java113
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeInfoTest.java42
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSectionTest.java105
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImplTest.java59
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java119
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java363
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdModuleTest.java38
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/HttpRequestIdModuleTest.java39
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdConfigurationTest.java33
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImplTest.java89
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/rule/CachingRuleFinderTest.java424
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoaderTest.java219
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDefinitionsLoaderTest.java126
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java128
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/rule/WebServerRuleFinderImplTest.java65
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/search/BaseDocTest.java159
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java88
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/startup/LogServerIdTest.java50
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterMetricsTest.java238
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPermissionTemplatesTest.java212
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java144
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java49
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/FakeServer.java98
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryClientTest.java82
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java376
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/updatecenter/UpdateCenterModuleTest.java35
-rw-r--r--server/sonar-webserver-core/src/test/java/org/sonar/server/util/DateCollectorTest.java45
-rw-r--r--server/sonar-webserver-core/src/test/resources/logback-test.xml26
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/csharp-model.xml25
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/java-model.xml25
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelXMLExporterTest/export_xml.xml20
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml50
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_deprecated_linear_with_threshold_function_by_linear_function.xml36
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_network_use_key.xml23
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/fail_on_bad_xml.xml1
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_deprecated_constant_per_file_function.xml25
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_invalid_value.xml28
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_remediation_cost_having_zero_value.xml71
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_rule_on_root_characteristics.xml19
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_badly_formatted_xml.xml43
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_constant_issue.xml29
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear.xml29
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_having_offset_to_zero.xml34
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_with_offset.xml54
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules.xml33
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules_with_deprecated_quality_model_format.xml58
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/read_integer.xml29
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/replace_mn_by_min.xml49
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/use_default_unit_when_no_unit.xml32
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/metric/DefaultMetricFinderTest/shared.xml12
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jarbin0 -> 2712 bytes
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/build.properties2
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/empty-version.txt0
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/version.txt1
-rw-r--r--server/sonar-webserver-core/src/test/resources/org/sonar/server/telemetry/telemetry-example.json55
199 files changed, 18484 insertions, 0 deletions
diff --git a/server/sonar-webserver-core/COPYING b/server/sonar-webserver-core/COPYING
new file mode 100644
index 00000000000..fc8a5de7edf
--- /dev/null
+++ b/server/sonar-webserver-core/COPYING
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/server/sonar-webserver-core/build.gradle b/server/sonar-webserver-core/build.gradle
new file mode 100644
index 00000000000..9ff120d6cfb
--- /dev/null
+++ b/server/sonar-webserver-core/build.gradle
@@ -0,0 +1,81 @@
+sonarqube {
+ properties {
+ property 'sonar.projectName', "${projectTitle} :: Web Server :: Core"
+ }
+}
+
+import org.apache.tools.ant.filters.ReplaceTokens
+processResources {
+ filesMatching('build.properties') {
+ filter ReplaceTokens, tokens: [
+ 'buildNumber': release ? 'git rev-parse HEAD'.execute().text.trim() : 'N/A'
+ ]
+ }
+}
+
+dependencies {
+ // please keep the list grouped by configuration and ordered by name
+
+ compile 'ch.qos.logback:logback-access'
+ compile 'ch.qos.logback:logback-classic'
+ compile 'ch.qos.logback:logback-core'
+ compile 'com.google.code.gson:gson'
+ compile 'com.google.protobuf:protobuf-java'
+ compile 'commons-dbutils:commons-dbutils'
+ compile 'io.jsonwebtoken:jjwt-api'
+ compile 'io.jsonwebtoken:jjwt-impl'
+ compile 'org.apache.httpcomponents:httpclient'
+ compile 'org.apache.logging.log4j:log4j-api'
+ compile 'org.apache.tomcat.embed:tomcat-embed-core'
+ compile 'org.apache.commons:commons-dbcp2'
+ compile 'org.picocontainer:picocontainer'
+ compile 'org.slf4j:jul-to-slf4j'
+ compile 'org.slf4j:slf4j-api'
+ compile 'org.sonarsource.update-center:sonar-update-center-common'
+ compile 'org.mindrot:jbcrypt'
+
+ compile project(':server:sonar-ce-common')
+ compile project(':server:sonar-ce-task')
+ compile project(':server:sonar-ce-task-projectanalysis')
+ compile project(':server:sonar-db-dao')
+ compile project(':server:sonar-db-migration')
+ compile project(':server:sonar-process')
+ compile project(':server:sonar-server-common')
+ compile project(':server:sonar-webserver-auth')
+ compile project(':server:sonar-webserver-common')
+ compile project(':server:sonar-webserver-es')
+ compile project(':sonar-core')
+ compile project(':sonar-duplications')
+ compile project(':sonar-scanner-protocol')
+ compile project(':sonar-markdown')
+ compile project(path: ':sonar-plugin-api', configuration: 'shadow')
+ compile project(':sonar-plugin-api-impl')
+ compile project(':sonar-ws')
+
+ compileOnly 'com.google.code.findbugs:jsr305'
+ // not a transitive dep. At runtime lib/jdbc/h2 is used
+ compileOnly 'com.h2database:h2'
+
+ testCompile 'com.github.kevinsawicki:http-request'
+ testCompile 'com.google.code.findbugs:jsr305'
+ testCompile 'com.squareup.okhttp3:mockwebserver'
+ testCompile 'com.tngtech.java:junit-dataprovider'
+ testCompile 'junit:junit'
+ testCompile 'org.apache.logging.log4j:log4j-api'
+ testCompile 'org.apache.logging.log4j:log4j-core'
+ testCompile 'org.assertj:assertj-core'
+ testCompile 'org.assertj:assertj-guava'
+ testCompile 'org.eclipse.jetty:jetty-server'
+ testCompile 'org.eclipse.jetty:jetty-servlet'
+ testCompile 'org.hamcrest:hamcrest-all'
+ testCompile 'org.mockito:mockito-core'
+ testCompile 'org.subethamail:subethasmtp'
+ testCompile project(':server:sonar-db-testing')
+ testCompile project(path: ":server:sonar-server-common", configuration: "tests")
+ testCompile project(path: ":server:sonar-webserver-auth", configuration: "tests")
+ testCompile project(path: ":server:sonar-webserver-es", configuration: "tests")
+ testCompile project(path: ":server:sonar-webserver-ws", configuration: "tests")
+ testCompile project(':sonar-testing-harness')
+
+ runtime 'io.jsonwebtoken:jjwt-jackson'
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/app/WebServerProcessLogging.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/app/WebServerProcessLogging.java
new file mode 100644
index 00000000000..9f866b62d70
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/app/WebServerProcessLogging.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.app;
+
+import ch.qos.logback.classic.Level;
+import org.sonar.process.ProcessId;
+import org.sonar.process.logging.LogDomain;
+import org.sonar.process.logging.LogLevelConfig;
+import org.sonar.server.log.ServerProcessLogging;
+
+import static org.sonar.server.platform.web.requestid.RequestIdMDCStorage.HTTP_REQUEST_ID_MDC_KEY;
+
+/**
+ * Configure logback for the Web Server process. Logs are written to file "web.log" in SQ's log directory.
+ */
+public class WebServerProcessLogging extends ServerProcessLogging {
+
+ public WebServerProcessLogging() {
+ super(ProcessId.WEB_SERVER, "%X{" + HTTP_REQUEST_ID_MDC_KEY + "}");
+ }
+
+ @Override
+ protected void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder) {
+ logLevelConfigBuilder.levelByDomain("sql", ProcessId.WEB_SERVER, LogDomain.SQL);
+ logLevelConfigBuilder.levelByDomain("es", ProcessId.WEB_SERVER, LogDomain.ES);
+ logLevelConfigBuilder.levelByDomain("auth.event", ProcessId.WEB_SERVER, LogDomain.AUTH_EVENT);
+ JMX_RMI_LOGGER_NAMES.forEach(loggerName -> logLevelConfigBuilder.levelByDomain(loggerName, ProcessId.WEB_SERVER, LogDomain.JMX));
+
+ logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.ContainerBase");
+ logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.StandardContext");
+ logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.StandardService");
+
+ LOGGER_NAMES_TO_TURN_OFF.forEach(loggerName -> logLevelConfigBuilder.immutableLevel(loggerName, Level.OFF));
+ }
+
+ @Override
+ protected void extendConfigure() {
+ // No extension needed
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelPluginRepository.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelPluginRepository.java
new file mode 100644
index 00000000000..016f20434b9
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelPluginRepository.java
@@ -0,0 +1,140 @@
+/*
+ * 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.debt;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import org.picocontainer.Startable;
+import org.sonar.api.Plugin;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginRepository;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * <p>This class is used to find which technical debt model XML files exist in the Sonar instance.</p>
+ * <p>
+ * Those XML files are provided by language plugins that embed their own contribution to the definition of the Technical debt model.
+ * They must be located in the classpath of those language plugins, more specifically in the "com.sonar.sqale" package, and
+ * they must be named "<pluginKey>-model.xml".
+ * </p>
+ */
+@ServerSide
+public class DebtModelPluginRepository implements Startable {
+
+ public static final String DEFAULT_MODEL = "technical-debt";
+
+ private static final String XML_FILE_SUFFIX = "-model.xml";
+ private static final String XML_FILE_PREFIX = "com/sonar/sqale/";
+
+ private String xmlFilePrefix;
+
+ private PluginRepository pluginRepository;
+ private Map<String, ClassLoader> contributingPluginKeyToClassLoader;
+
+ public DebtModelPluginRepository(PluginRepository pluginRepository) {
+ this.pluginRepository = pluginRepository;
+ this.xmlFilePrefix = XML_FILE_PREFIX;
+ }
+
+ @VisibleForTesting
+ DebtModelPluginRepository(PluginRepository pluginRepository, String xmlFilePrefix) {
+ this.pluginRepository = pluginRepository;
+ this.xmlFilePrefix = xmlFilePrefix;
+ }
+
+ @VisibleForTesting
+ DebtModelPluginRepository(Map<String, ClassLoader> contributingPluginKeyToClassLoader, String xmlFilePrefix) {
+ this.contributingPluginKeyToClassLoader = contributingPluginKeyToClassLoader;
+ this.xmlFilePrefix = xmlFilePrefix;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void start() {
+ findAvailableXMLFiles();
+ }
+
+ private void findAvailableXMLFiles() {
+ if (contributingPluginKeyToClassLoader == null) {
+ contributingPluginKeyToClassLoader = Maps.newTreeMap();
+ // Add default model
+ contributingPluginKeyToClassLoader.put(DEFAULT_MODEL, getClass().getClassLoader());
+ for (PluginInfo pluginInfo : pluginRepository.getPluginInfos()) {
+ String pluginKey = pluginInfo.getKey();
+ Plugin plugin = pluginRepository.getPluginInstance(pluginKey);
+ ClassLoader classLoader = plugin.getClass().getClassLoader();
+ if (classLoader.getResource(getXMLFilePath(pluginKey)) != null) {
+ contributingPluginKeyToClassLoader.put(pluginKey, classLoader);
+ }
+ }
+ }
+ contributingPluginKeyToClassLoader = Collections.unmodifiableMap(contributingPluginKeyToClassLoader);
+ }
+
+ @VisibleForTesting
+ String getXMLFilePath(String model) {
+ return xmlFilePrefix + model + XML_FILE_SUFFIX;
+ }
+
+ /**
+ * Returns the list of plugins that can contribute to the technical debt model.
+ *
+ * @return the list of plugin keys
+ */
+ public Collection<String> getContributingPluginList() {
+ return newArrayList(contributingPluginKeyToClassLoader.keySet());
+ }
+
+ /**
+ * Creates a new {@link java.io.Reader} for the XML file that contains the model contributed by the given plugin.
+ *
+ * @param pluginKey the key of the plugin that contributes the XML file
+ * @return the reader, that must be closed once its use is finished.
+ */
+ public Reader createReaderForXMLFile(String pluginKey) {
+ ClassLoader classLoader = contributingPluginKeyToClassLoader.get(pluginKey);
+ String xmlFilePath = getXMLFilePath(pluginKey);
+ return new InputStreamReader(classLoader.getResourceAsStream(xmlFilePath), StandardCharsets.UTF_8);
+ }
+
+ @VisibleForTesting
+ Map<String, ClassLoader> getContributingPluginKeyToClassLoader() {
+ return contributingPluginKeyToClassLoader;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelXMLExporter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelXMLExporter.java
new file mode 100644
index 00000000000..5536f1baf7f
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelXMLExporter.java
@@ -0,0 +1,204 @@
+/*
+ * 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.debt;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.xml.XMLConstants;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.ServerSide;
+import org.xml.sax.InputSource;
+
+/**
+ * Export characteristics and rule debt definitions to XML
+ */
+@ServerSide
+public class DebtModelXMLExporter {
+
+ private static final String ROOT = "sqale";
+ private static final String DEFAULT_INDENT = "2";
+
+ public static final String CHARACTERISTIC = "chc";
+ public static final String PROPERTY = "prop";
+ public static final String PROPERTY_KEY = "key";
+ public static final String PROPERTY_VALUE = "val";
+ public static final String PROPERTY_TEXT_VALUE = "txt";
+
+ public static final String REPOSITORY_KEY = "rule-repo";
+ public static final String RULE_KEY = "rule-key";
+
+ public static final String PROPERTY_FUNCTION = "remediationFunction";
+ public static final String PROPERTY_COEFFICIENT = "remediationFactor";
+ public static final String PROPERTY_OFFSET = "offset";
+
+ protected String export(List<RuleDebt> allRules) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<" + ROOT + ">");
+ for (RuleDebt rule : allRules) {
+ processRule(rule, sb);
+ }
+ sb.append("</" + ROOT + ">");
+ String xml = sb.toString();
+ xml = prettyFormatXml(xml);
+ return xml;
+ }
+
+ private static void processRule(RuleDebt rule, StringBuilder xml) {
+ xml.append("<" + CHARACTERISTIC + ">");
+ xml.append("<" + REPOSITORY_KEY + ">");
+ xml.append(StringEscapeUtils.escapeXml(rule.ruleKey().repository()));
+ xml.append("</" + REPOSITORY_KEY + "><" + RULE_KEY + ">");
+ xml.append(StringEscapeUtils.escapeXml(rule.ruleKey().rule()));
+ xml.append("</" + RULE_KEY + ">");
+
+ String coefficient = rule.coefficient();
+ String offset = rule.offset();
+
+ processProperty(PROPERTY_FUNCTION, null, rule.function(), xml);
+ if (coefficient != null) {
+ String[] values = getValues(coefficient);
+ processProperty(PROPERTY_COEFFICIENT, values[0], values[1], xml);
+ }
+ if (offset != null) {
+ String[] values = getValues(offset);
+ processProperty(PROPERTY_OFFSET, values[0], values[1], xml);
+ }
+ xml.append("</" + CHARACTERISTIC + ">");
+ }
+
+ private static String[] getValues(String factorOrOffset) {
+ String[] result = new String[2];
+ Pattern pattern = Pattern.compile("(\\d+)(\\w+)");
+ Matcher matcher = pattern.matcher(factorOrOffset);
+ if (matcher.find()) {
+ String value = matcher.group(1);
+ String unit = matcher.group(2);
+ result[0] = value;
+ result[1] = unit;
+ }
+ return result;
+ }
+
+ private static void processProperty(String key, @Nullable String val, String text, StringBuilder xml) {
+ xml.append("<" + PROPERTY + "><" + PROPERTY_KEY + ">");
+ xml.append(StringEscapeUtils.escapeXml(key));
+ xml.append("</" + PROPERTY_KEY + ">");
+ if (val != null) {
+ xml.append("<" + PROPERTY_VALUE + ">");
+ xml.append(val);
+ xml.append("</" + PROPERTY_VALUE + ">");
+ }
+ if (StringUtils.isNotEmpty(text)) {
+ xml.append("<" + PROPERTY_TEXT_VALUE + ">");
+ xml.append(StringEscapeUtils.escapeXml(text));
+ xml.append("</" + PROPERTY_TEXT_VALUE + ">");
+ }
+ xml.append("</" + PROPERTY + ">");
+ }
+
+ private static String prettyFormatXml(String xml) {
+ try {
+ TransformerFactory factory = SAXTransformerFactory.newInstance();
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ Transformer serializer = factory.newTransformer();
+ serializer.setOutputProperty(OutputKeys.INDENT, "yes");
+ serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", DEFAULT_INDENT);
+ Source xmlSource = new SAXSource(new InputSource(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
+ StreamResult res = new StreamResult(new ByteArrayOutputStream());
+ serializer.transform(xmlSource, res);
+ return new String(((ByteArrayOutputStream) res.getOutputStream()).toByteArray(), StandardCharsets.UTF_8);
+ } catch (TransformerException ignored) {
+ // Ignore, raw XML will be returned
+ }
+ return xml;
+ }
+
+ public static class RuleDebt {
+ private RuleKey ruleKey;
+ private String function;
+ private String coefficient;
+ private String offset;
+
+ public RuleKey ruleKey() {
+ return ruleKey;
+ }
+
+ public RuleDebt setRuleKey(RuleKey ruleKey) {
+ this.ruleKey = ruleKey;
+ return this;
+ }
+
+ public String function() {
+ return function;
+ }
+
+ public RuleDebt setFunction(String function) {
+ this.function = function;
+ return this;
+ }
+
+ @CheckForNull
+ public String coefficient() {
+ return coefficient;
+ }
+
+ public RuleDebt setCoefficient(@Nullable String coefficient) {
+ this.coefficient = coefficient;
+ return this;
+ }
+
+ @CheckForNull
+ public String offset() {
+ return offset;
+ }
+
+ public RuleDebt setOffset(@Nullable String offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "RuleDebt{" +
+ "ruleKey=" + ruleKey +
+ ", function=" + function +
+ ", coefficient='" + coefficient + '\'' +
+ ", offset='" + offset + '\'' +
+ '}';
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtRulesXMLImporter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtRulesXMLImporter.java
new file mode 100644
index 00000000000..18d7dc1168d
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtRulesXMLImporter.java
@@ -0,0 +1,272 @@
+/*
+ * 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.debt;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.math.NumberUtils;
+import org.codehaus.stax2.XMLInputFactory2;
+import org.codehaus.staxmate.SMInputFactory;
+import org.codehaus.staxmate.in.SMHierarchicCursor;
+import org.codehaus.staxmate.in.SMInputCursor;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.Duration;
+import org.sonar.api.utils.ValidationMessages;
+import org.sonar.server.debt.DebtModelXMLExporter.RuleDebt;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static javax.xml.stream.XMLInputFactory.IS_COALESCING;
+import static javax.xml.stream.XMLInputFactory.IS_NAMESPACE_AWARE;
+import static javax.xml.stream.XMLInputFactory.IS_VALIDATING;
+import static javax.xml.stream.XMLInputFactory.SUPPORT_DTD;
+import static org.apache.commons.lang.StringUtils.isNotBlank;
+import static org.sonar.api.server.debt.DebtRemediationFunction.Type.CONSTANT_ISSUE;
+import static org.sonar.api.server.debt.DebtRemediationFunction.Type.LINEAR;
+import static org.sonar.api.utils.Duration.MINUTE;
+import static org.sonar.server.debt.DebtModelXMLExporter.CHARACTERISTIC;
+import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY;
+import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_COEFFICIENT;
+import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_FUNCTION;
+import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_KEY;
+import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_OFFSET;
+import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_TEXT_VALUE;
+import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_VALUE;
+import static org.sonar.server.debt.DebtModelXMLExporter.REPOSITORY_KEY;
+import static org.sonar.server.debt.DebtModelXMLExporter.RULE_KEY;
+
+/**
+ * Import rules debt definitions from an XML
+ */
+@ServerSide
+public class DebtRulesXMLImporter {
+
+ public List<RuleDebt> importXML(String xml, ValidationMessages validationMessages) {
+ return importXML(new StringReader(xml), validationMessages);
+ }
+
+ public List<RuleDebt> importXML(Reader xml, ValidationMessages validationMessages) {
+ List<RuleDebt> ruleDebts = newArrayList();
+ try {
+ SMInputFactory inputFactory = initStax();
+ SMHierarchicCursor cursor = inputFactory.rootElementCursor(xml);
+
+ // advance to <sqale>
+ cursor.advance();
+ SMInputCursor rootCursor = cursor.childElementCursor(CHARACTERISTIC);
+ while (rootCursor.getNext() != null) {
+ process(ruleDebts, validationMessages, rootCursor);
+ }
+
+ cursor.getStreamReader().closeCompletely();
+ } catch (XMLStreamException e) {
+ throw new IllegalStateException("XML is not valid", e);
+ }
+ return ruleDebts;
+ }
+
+ private static SMInputFactory initStax() {
+ XMLInputFactory xmlFactory = XMLInputFactory2.newInstance();
+ xmlFactory.setProperty(IS_COALESCING, TRUE);
+ xmlFactory.setProperty(IS_NAMESPACE_AWARE, FALSE);
+ xmlFactory.setProperty(SUPPORT_DTD, FALSE);
+ xmlFactory.setProperty(IS_VALIDATING, FALSE);
+ return new SMInputFactory(xmlFactory);
+ }
+
+ private static void process(List<RuleDebt> ruleDebts,
+ ValidationMessages validationMessages, SMInputCursor chcCursor) throws XMLStreamException {
+ SMInputCursor cursor = chcCursor.childElementCursor();
+ while (cursor.getNext() != null) {
+ String node = cursor.getLocalName();
+ if (StringUtils.equals(node, CHARACTERISTIC)) {
+ process(ruleDebts, validationMessages, cursor);
+ } else if (StringUtils.equals(node, REPOSITORY_KEY)) {
+ RuleDebt ruleDebt = processRule(validationMessages, cursor);
+ if (ruleDebt != null) {
+ ruleDebts.add(ruleDebt);
+ }
+ }
+ }
+ }
+
+ @CheckForNull
+ private static RuleDebt processRule(ValidationMessages validationMessages, SMInputCursor cursor) throws XMLStreamException {
+ String ruleRepositoryKey = cursor.collectDescendantText().trim();
+ String ruleKey = null;
+ Properties properties = new Properties();
+ while (cursor.getNext() != null) {
+ String node = cursor.getLocalName();
+ if (StringUtils.equals(node, PROPERTY)) {
+ properties.add(processProperty(validationMessages, cursor));
+ } else if (StringUtils.equals(node, RULE_KEY)) {
+ ruleKey = cursor.collectDescendantText().trim();
+ }
+ }
+ if (isNotBlank(ruleRepositoryKey) && isNotBlank(ruleKey)) {
+ return createRule(RuleKey.of(ruleRepositoryKey, ruleKey), properties, validationMessages);
+ }
+ return null;
+ }
+
+ private static Property processProperty(ValidationMessages validationMessages, SMInputCursor cursor) throws XMLStreamException {
+ SMInputCursor c = cursor.childElementCursor();
+ String key = null;
+ int value = 0;
+ String textValue = null;
+ while (c.getNext() != null) {
+ String node = c.getLocalName();
+ if (StringUtils.equals(node, PROPERTY_KEY)) {
+ key = c.collectDescendantText().trim();
+
+ } else if (StringUtils.equals(node, PROPERTY_VALUE)) {
+ String s = c.collectDescendantText().trim();
+ try {
+ Double valueDouble = NumberUtils.createDouble(s);
+ value = valueDouble.intValue();
+ } catch (NumberFormatException ex) {
+ validationMessages.addErrorText(String.format("Cannot import value '%s' for field %s - Expected a numeric value instead", s, key));
+ }
+ } else if (StringUtils.equals(node, PROPERTY_TEXT_VALUE)) {
+ textValue = c.collectDescendantText().trim();
+ textValue = "mn".equals(textValue) ? MINUTE : textValue;
+ }
+ }
+ return new Property(key, value, textValue);
+ }
+
+ @CheckForNull
+ private static RuleDebt createRule(RuleKey ruleKey, Properties properties, ValidationMessages validationMessages) {
+ Property function = properties.function();
+ Property coefficientProperty = properties.coefficient();
+ String coefficient = coefficientProperty == null ? null : coefficientProperty.toDuration();
+ Property offsetProperty = properties.offset();
+ String offset = offsetProperty == null ? null : offsetProperty.toDuration();
+ if (function != null && (coefficient != null || offset != null)) {
+ return createRuleDebt(ruleKey, function.getTextValue(), coefficient, offset, validationMessages);
+ }
+ return null;
+ }
+
+ @CheckForNull
+ private static RuleDebt createRuleDebt(RuleKey ruleKey, String function, @Nullable String coefficient, @Nullable String offset, ValidationMessages validationMessages) {
+ if ("constant_resource".equals(function)) {
+ validationMessages.addWarningText(String.format("Constant/file function is no longer used, technical debt definitions on '%s' are ignored.", ruleKey));
+ return null;
+ }
+ if ("linear_threshold".equals(function) && coefficient != null) {
+ validationMessages.addWarningText(String.format("Linear with threshold function is no longer used, remediation function of '%s' is replaced by linear.", ruleKey));
+ return createRuleDebt(ruleKey, LINEAR.name(), coefficient, null, validationMessages);
+ }
+ if (CONSTANT_ISSUE.name().equalsIgnoreCase(function) && coefficient != null && offset == null) {
+ return createRuleDebt(ruleKey, CONSTANT_ISSUE.name(), null, coefficient, validationMessages);
+ }
+ return new RuleDebt().setRuleKey(ruleKey).setFunction(function.toUpperCase()).setCoefficient(coefficient).setOffset(offset);
+ }
+
+ private static class Properties {
+ List<Property> list;
+
+ public Properties() {
+ this.list = newArrayList();
+ }
+
+ public Properties add(Property property) {
+ this.list.add(property);
+ return this;
+ }
+
+ public Property function() {
+ return find(PROPERTY_FUNCTION);
+ }
+
+ public Property coefficient() {
+ return find(PROPERTY_COEFFICIENT);
+ }
+
+ public Property offset() {
+ return find(PROPERTY_OFFSET);
+ }
+
+ private Property find(String key) {
+ return Iterables.find(list, new PropertyMatchKey(key), null);
+ }
+ }
+
+ private static class Property {
+ String key;
+ int value;
+ String textValue;
+
+ private Property(String key, int value, String textValue) {
+ this.key = key;
+ this.value = value;
+ this.textValue = textValue;
+ }
+
+ private String getKey() {
+ return key;
+ }
+
+ private int getValue() {
+ return value;
+ }
+
+ private String getTextValue() {
+ return textValue;
+ }
+
+ @CheckForNull
+ public String toDuration() {
+ if (key != null && getValue() > 0) {
+ String duration = Integer.toString(getValue());
+ duration += !Strings.isNullOrEmpty(getTextValue()) ? getTextValue() : Duration.DAY;
+ return duration;
+ }
+ return null;
+ }
+ }
+
+ private static class PropertyMatchKey implements Predicate<Property> {
+ private final String key;
+
+ public PropertyMatchKey(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public boolean apply(@Nonnull Property input) {
+ return input.getKey().equals(key);
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/package-info.java
new file mode 100644
index 00000000000..6610578a903
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/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.debt;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/es/IndexerStartupTask.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/IndexerStartupTask.java
new file mode 100644
index 00000000000..be64ce0a057
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/IndexerStartupTask.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.es;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.unit.TimeValue;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.server.es.metadata.MetadataIndex;
+
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toSet;
+
+public class IndexerStartupTask {
+
+ private static final Logger LOG = Loggers.get(IndexerStartupTask.class);
+
+ private final EsClient esClient;
+ private final Configuration config;
+ private final MetadataIndex metadataIndex;
+ private final StartupIndexer[] indexers;
+
+ public IndexerStartupTask(EsClient esClient, Configuration config, MetadataIndex metadataIndex, StartupIndexer... indexers) {
+ this.esClient = esClient;
+ this.config = config;
+ this.metadataIndex = metadataIndex;
+ this.indexers = indexers;
+ }
+
+ public void execute() {
+ if (indexesAreEnabled()) {
+ stream(indexers)
+ .forEach(this::indexUninitializedTypes);
+ }
+ }
+
+ private boolean indexesAreEnabled() {
+ return !config.getBoolean("sonar.internal.es.disableIndexes").orElse(false);
+ }
+
+ private void indexUninitializedTypes(StartupIndexer indexer) {
+ Set<IndexType> uninitializedTypes = getUninitializedTypes(indexer);
+ if (!uninitializedTypes.isEmpty()) {
+ Profiler profiler = Profiler.create(LOG);
+ profiler.startInfo(getLogMessage(uninitializedTypes, "..."));
+ indexer.indexOnStartup(uninitializedTypes);
+ uninitializedTypes.forEach(this::setInitialized);
+ profiler.stopInfo(getLogMessage(uninitializedTypes, "done"));
+ }
+ }
+
+ private Set<IndexType> getUninitializedTypes(StartupIndexer indexer) {
+ return indexer.getIndexTypes().stream()
+ .filter(indexType -> !metadataIndex.getInitialized(indexType))
+ .collect(toSet());
+ }
+
+ private void setInitialized(IndexType indexType) {
+ waitForIndexYellow(indexType.getMainType().getIndex().getName());
+ metadataIndex.setInitialized(indexType, true);
+ }
+
+ private void waitForIndexYellow(String index) {
+ Client nativeClient = esClient.nativeClient();
+ ClusterHealthAction.INSTANCE.newRequestBuilder(nativeClient).setIndices(index).setWaitForYellowStatus().get(TimeValue.timeValueMinutes(10));
+ }
+
+ private String getLogMessage(Set<IndexType> emptyTypes, String suffix) {
+ String s = emptyTypes.size() == 1 ? "" : "s";
+ String typeList = emptyTypes.stream().map(Object::toString).collect(Collectors.joining(","));
+ return String.format("Indexing of type%s %s %s", s, typeList, suffix);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/es/MigrationEsClientImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/MigrationEsClientImpl.java
new file mode 100644
index 00000000000..d353a266466
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/MigrationEsClientImpl.java
@@ -0,0 +1,77 @@
+/*
+ * 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.es;
+
+import com.google.common.collect.Maps;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.elasticsearch.action.admin.indices.stats.IndexStats;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.platform.db.migration.es.MigrationEsClient;
+
+public class MigrationEsClientImpl implements MigrationEsClient {
+ private final EsClient client;
+ private final Set<String> updatedIndices = new HashSet<>();
+
+ public MigrationEsClientImpl(EsClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public void deleteIndexes(String name, String... otherNames) {
+ Map<String, IndexStats> indices = client.nativeClient().admin().indices().prepareStats().get().getIndices();
+ Set<String> existingIndices = indices.values().stream().map(IndexStats::getIndex).collect(MoreCollectors.toSet());
+ Stream.concat(Stream.of(name), Arrays.stream(otherNames))
+ .distinct()
+ .filter(existingIndices::contains)
+ .forEach(this::deleteIndex);
+ }
+
+ @Override
+ public void addMappingToExistingIndex(String index, String type, String mappingName, String mappingType, Map<String, String> options) {
+ IndexStats stats = client.nativeClient().admin().indices().prepareStats().get().getIndex(index);
+ if (stats != null) {
+ Loggers.get(getClass()).info("Add mapping [{}] to Elasticsearch index [{}]", mappingName, index);
+ String mappingOptions = Stream.concat(Stream.of(Maps.immutableEntry("type", mappingType)), options.entrySet().stream())
+ .map(e -> e.getKey() + "=" + e.getValue())
+ .collect(Collectors.joining(","));
+ client.nativeClient().admin().indices().preparePutMapping(index)
+ .setType(type)
+ .setSource(mappingName, mappingOptions)
+ .get();
+ updatedIndices.add(index);
+ }
+ }
+
+ @Override
+ public Set<String> getUpdatedIndices() {
+ return updatedIndices;
+ }
+
+ private void deleteIndex(String index) {
+ Loggers.get(getClass()).info("Drop Elasticsearch index [{}]", index);
+ client.nativeClient().admin().indices().prepareDelete(index).get();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/es/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/package-info.java
new file mode 100644
index 00000000000..df6be07802c
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/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.es;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationDaemon.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationDaemon.java
new file mode 100644
index 00000000000..acf90de9586
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationDaemon.java
@@ -0,0 +1,138 @@
+/*
+ * 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.notification;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.picocontainer.Startable;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.util.Collections.singleton;
+
+@Properties({
+ @Property(
+ key = NotificationDaemon.PROPERTY_DELAY,
+ defaultValue = "60",
+ name = "Delay of notifications, in seconds",
+ global = false),
+ @Property(
+ key = NotificationDaemon.PROPERTY_DELAY_BEFORE_REPORTING_STATUS,
+ defaultValue = "600",
+ name = "Delay before reporting notification status, in seconds",
+ global = false)
+})
+@ServerSide
+public class NotificationDaemon implements Startable {
+ private static final String THREAD_NAME_PREFIX = "sq-notification-service-";
+
+ private static final Logger LOG = Loggers.get(NotificationDaemon.class);
+
+ public static final String PROPERTY_DELAY = "sonar.notifications.delay";
+ public static final String PROPERTY_DELAY_BEFORE_REPORTING_STATUS = "sonar.notifications.runningDelayBeforeReportingStatus";
+
+ private final long delayInSeconds;
+ private final long delayBeforeReportingStatusInSeconds;
+ private final DefaultNotificationManager manager;
+ private final NotificationService service;
+
+ private ScheduledExecutorService executorService;
+ private boolean stopping = false;
+
+ public NotificationDaemon(Configuration config, DefaultNotificationManager manager, NotificationService service) {
+ this.delayInSeconds = config.getLong(PROPERTY_DELAY).get();
+ this.delayBeforeReportingStatusInSeconds = config.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS).get();
+ this.manager = manager;
+ this.service = service;
+ }
+
+ @Override
+ public void start() {
+ executorService = Executors.newSingleThreadScheduledExecutor(
+ new ThreadFactoryBuilder()
+ .setNameFormat(THREAD_NAME_PREFIX + "%d")
+ .setPriority(Thread.MIN_PRIORITY)
+ .build());
+ executorService.scheduleWithFixedDelay(() -> {
+ try {
+ processQueue();
+ } catch (Exception e) {
+ LOG.error("Error in NotificationService", e);
+ }
+ }, 0, delayInSeconds, TimeUnit.SECONDS);
+ LOG.info("Notification service started (delay {} sec.)", delayInSeconds);
+ }
+
+ @Override
+ public void stop() {
+ try {
+ stopping = true;
+ executorService.shutdown();
+ executorService.awaitTermination(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ LOG.error("Error during stop of notification service", e);
+ Thread.currentThread().interrupt();
+ }
+ LOG.info("Notification service stopped");
+ }
+
+ private synchronized void processQueue() {
+ long start = now();
+ long lastLog = start;
+ long notifSentCount = 0;
+
+ Notification notifToSend = manager.getFromQueue();
+ while (notifToSend != null) {
+ notifSentCount += service.deliverEmails(singleton(notifToSend));
+ // compatibility with old API
+ notifSentCount += service.deliver(notifToSend);
+ if (stopping) {
+ break;
+ }
+ long now = now();
+ if (now - lastLog > delayBeforeReportingStatusInSeconds * 1000) {
+ long remainingNotifCount = manager.count();
+ lastLog = now;
+ long spentTimeInMinutes = (now - start) / (60 * 1000);
+ log(notifSentCount, remainingNotifCount, spentTimeInMinutes);
+ }
+ notifToSend = manager.getFromQueue();
+ }
+ }
+
+ @VisibleForTesting
+ void log(long notifSentCount, long remainingNotifCount, long spentTimeInMinutes) {
+ LOG.info("{} notifications sent during the past {} minutes and {} still waiting to be sent",
+ notifSentCount, spentTimeInMinutes, remainingNotifCount);
+ }
+
+ @VisibleForTesting
+ long now() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationModule.java
new file mode 100644
index 00000000000..abe04c57386
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationModule.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.
+ */
+package org.sonar.server.notification;
+
+import org.sonar.api.config.EmailSettings;
+import org.sonar.core.platform.Module;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+
+public class NotificationModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ EmailSettings.class,
+ NotificationService.class,
+ DefaultNotificationManager.class,
+ NotificationDaemon.class,
+ EmailNotificationChannel.class);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/package-info.java
new file mode 100644
index 00000000000..a38c847a25f
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/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.notification;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/AbstractSystemInfoWriter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/AbstractSystemInfoWriter.java
new file mode 100644
index 00000000000..fad900559ed
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/AbstractSystemInfoWriter.java
@@ -0,0 +1,93 @@
+/*
+ * 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 java.util.Collection;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.process.systeminfo.SystemInfoUtils;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.health.Health;
+import org.sonar.server.telemetry.TelemetryDataLoader;
+
+import static org.sonar.server.telemetry.TelemetryDataJsonWriter.writeTelemetryData;
+
+public abstract class AbstractSystemInfoWriter implements SystemInfoWriter {
+ private static final String[] ORDERED_SECTION_NAMES = {
+ // standalone
+ "System", "Database", "Plugins",
+
+ // cluster
+ "Web JVM State", "Web Database Connection", "Web Logging", "Web JVM Properties",
+ "Compute Engine Tasks", "Compute Engine JVM State", "Compute Engine Database Connection", "Compute Engine Logging", "Compute Engine JVM Properties",
+ "Search State", "Search Indexes"};
+
+ private final TelemetryDataLoader telemetry;
+
+ AbstractSystemInfoWriter(TelemetryDataLoader telemetry) {
+ this.telemetry = telemetry;
+ }
+
+ protected void writeSections(Collection<ProtobufSystemInfo.Section> sections, JsonWriter json) {
+ SystemInfoUtils
+ .order(sections, ORDERED_SECTION_NAMES)
+ .forEach(section -> writeSection(section, json));
+ }
+
+ private void writeSection(ProtobufSystemInfo.Section section, JsonWriter json) {
+ json.name(section.getName());
+ json.beginObject();
+ for (ProtobufSystemInfo.Attribute attribute : section.getAttributesList()) {
+ writeAttribute(attribute, json);
+ }
+ json.endObject();
+ }
+
+ private void writeAttribute(ProtobufSystemInfo.Attribute attribute, JsonWriter json) {
+ switch (attribute.getValueCase()) {
+ case BOOLEAN_VALUE:
+ json.prop(attribute.getKey(), attribute.getBooleanValue());
+ break;
+ case LONG_VALUE:
+ json.prop(attribute.getKey(), attribute.getLongValue());
+ break;
+ case DOUBLE_VALUE:
+ json.prop(attribute.getKey(), attribute.getDoubleValue());
+ break;
+ case STRING_VALUE:
+ json.prop(attribute.getKey(), attribute.getStringValue());
+ break;
+ case VALUE_NOT_SET:
+ json.name(attribute.getKey()).beginArray().values(attribute.getStringValuesList()).endArray();
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported type: " + attribute.getValueCase());
+ }
+ }
+
+ protected void writeHealth(Health health, JsonWriter json) {
+ json.prop("Health", health.getStatus().name());
+ json.name("Health Causes").beginArray().values(health.getCauses()).endArray();
+ }
+
+ protected void writeTelemetry(JsonWriter json) {
+ json.name("Statistics");
+ writeTelemetryData(json, telemetry.load());
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/BackendCleanup.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/BackendCleanup.java
new file mode 100644
index 00000000000..e4fc7c2bce6
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/BackendCleanup.java
@@ -0,0 +1,249 @@
+/*
+ * 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 com.google.common.collect.ImmutableMap;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.dialect.Oracle;
+import org.sonar.db.version.SqTables;
+import org.sonar.server.component.index.ComponentIndexDefinition;
+import org.sonar.server.es.BulkIndexer;
+import org.sonar.server.es.EsClient;
+import org.sonar.server.es.Index;
+import org.sonar.server.es.IndexType;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition;
+import org.sonar.server.property.InternalProperties;
+import org.sonar.server.view.index.ViewIndexDefinition;
+
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+
+@ServerSide
+public class BackendCleanup {
+
+ private static final String[] ANALYSIS_TABLES = {
+ "ce_activity", "ce_queue", "ce_task_input", "ce_scanner_context",
+ "duplications_index", "events", "issues", "issue_changes", "manual_measures",
+ "notifications", "project_links", "project_measures", "projects",
+ "snapshots", "file_sources", "webhook_deliveries"
+ };
+ private static final String[] RESOURCE_RELATED_TABLES = {
+ "group_roles", "user_roles", "properties"
+ };
+ private static final Map<String, TableCleaner> TABLE_CLEANERS = ImmutableMap.of(
+ "organizations", BackendCleanup::truncateOrganizations,
+ "users", BackendCleanup::truncateUsers,
+ "groups", BackendCleanup::truncateGroups,
+ "internal_properties", BackendCleanup::truncateInternalProperties,
+ "schema_migrations", BackendCleanup::truncateSchemaMigrations);
+
+ private final EsClient esClient;
+ private final DbClient dbClient;
+
+ public BackendCleanup(EsClient esClient, DbClient dbClient) {
+ this.esClient = esClient;
+ this.dbClient = dbClient;
+ }
+
+ public void clearAll() {
+ clearDb();
+ clearIndexes();
+ }
+
+ public void clearDb() {
+ try (DbSession dbSession = dbClient.openSession(false);
+ Connection connection = dbSession.getConnection();
+ Statement ddlStatement = connection.createStatement()) {
+ for (String tableName : SqTables.TABLES) {
+ Optional.ofNullable(TABLE_CLEANERS.get(tableName))
+ .orElse(BackendCleanup::truncateDefault)
+ .clean(tableName, ddlStatement, connection);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to clear db", e);
+ }
+ }
+
+ public void clearIndexes() {
+ Loggers.get(getClass()).info("Truncate Elasticsearch indices");
+ try {
+ esClient.prepareClearCache().get();
+
+ for (String index : esClient.prepareState().get().getState().getMetaData().getConcreteAllIndices()) {
+ /*under the hood, type is not used to perform clearIndex, so it's ok it does not match any existing index*/
+ clearIndex(Index.simple(index));
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to clear indexes", e);
+ }
+ }
+
+ /**
+ * Reset data in order to to be in same state as a fresh installation (but without having to drop db and restart the server).
+ *
+ * Please be careful when updating this method as it's called by Orchestrator.
+ */
+ public void resetData() {
+ try (DbSession dbSession = dbClient.openSession(false);
+ Connection connection = dbSession.getConnection()) {
+
+ truncateAnalysisTables(connection);
+ deleteManualRules(connection);
+ truncateInternalProperties(null, null, connection);
+ truncateUsers(null, null, connection);
+ truncateOrganizations(null, null, connection);
+ } catch (SQLException e) {
+ throw new IllegalStateException("Fail to reset data", e);
+ }
+
+ clearIndex(IssueIndexDefinition.DESCRIPTOR);
+ clearIndex(ViewIndexDefinition.DESCRIPTOR);
+ clearIndex(ProjectMeasuresIndexDefinition.DESCRIPTOR);
+ clearIndex(ComponentIndexDefinition.DESCRIPTOR);
+ }
+
+ private void truncateAnalysisTables(Connection connection) throws SQLException {
+ try (Statement statement = connection.createStatement()) {
+ // Clear inspection tables
+ for (String table : ANALYSIS_TABLES) {
+ statement.execute(createTruncateSql(table.toLowerCase(Locale.ENGLISH)));
+ // commit is useless on some databases
+ connection.commit();
+ }
+ // Clear resource related tables
+ for (String table : RESOURCE_RELATED_TABLES) {
+ statement.execute("DELETE FROM " + table + " WHERE resource_id IS NOT NULL");
+ connection.commit();
+ }
+ }
+ }
+
+ private String createTruncateSql(String table) {
+ if (dbClient.getDatabase().getDialect().getId().equals(Oracle.ID)) {
+ // truncate operation is needs to lock the table on Oracle. Unfortunately
+ // it fails sometimes in our QA environment because table is locked.
+ // We never found the root cause (no locks found when displaying them just after
+ // receiving the error).
+ // Workaround is to use "delete" operation. It does not require lock on table.
+ return "DELETE FROM " + table;
+ }
+ return "TRUNCATE TABLE " + table;
+ }
+
+ private static void deleteManualRules(Connection connection) throws SQLException {
+ try (PreparedStatement statement = connection.prepareStatement("DELETE FROM rules WHERE rules.plugin_name='manual'")) {
+ statement.execute();
+ // commit is useless on some databases
+ connection.commit();
+ }
+ }
+
+ /**
+ * Completely remove a index with all types
+ */
+ public void clearIndex(Index index) {
+ BulkIndexer.delete(esClient, IndexType.main(index, index.getName()), esClient.prepareSearch(index).setQuery(matchAllQuery()));
+ }
+
+ @FunctionalInterface
+ private interface TableCleaner {
+ void clean(String tableName, Statement ddlStatement, Connection connection) throws SQLException;
+ }
+
+ private static void truncateDefault(String tableName, Statement ddlStatement, Connection connection) throws SQLException {
+ ddlStatement.execute("TRUNCATE TABLE " + tableName.toLowerCase(Locale.ENGLISH));
+ // commit is useless on some databases
+ connection.commit();
+ }
+
+ /**
+ * Default organization must never be deleted
+ */
+ private static void truncateOrganizations(String tableName, Statement ddlStatement, Connection connection) throws SQLException {
+ try (PreparedStatement preparedStatement = connection.prepareStatement("delete from organizations where kee <> ?")) {
+ preparedStatement.setString(1, "default-organization");
+ preparedStatement.execute();
+ // commit is useless on some databases
+ connection.commit();
+ }
+ }
+
+ /**
+ * User admin must never be deleted.
+ */
+ private static void truncateUsers(String tableName, Statement ddlStatement, Connection connection) throws SQLException {
+ try (PreparedStatement preparedStatement = connection.prepareStatement("delete from users where login <> ?")) {
+ preparedStatement.setString(1, "admin");
+ preparedStatement.execute();
+ // commit is useless on some databases
+ connection.commit();
+ }
+ // "admin" is not flagged as root by default
+ try (PreparedStatement preparedStatement = connection.prepareStatement("update users set is_root=?")) {
+ preparedStatement.setBoolean(1, false);
+ preparedStatement.execute();
+ // commit is useless on some databases
+ connection.commit();
+ }
+ }
+
+ /**
+ * Groups sonar-users is referenced by the default organization as its default group.
+ */
+ private static void truncateGroups(String tableName, Statement ddlStatement, Connection connection) throws SQLException {
+ try (PreparedStatement preparedStatement = connection.prepareStatement("delete from groups where name <> ?")) {
+ preparedStatement.setString(1, "sonar-users");
+ preparedStatement.execute();
+ // commit is useless on some databases
+ connection.commit();
+ }
+ }
+
+ /**
+ * Internal property {@link InternalProperties#DEFAULT_ORGANIZATION} must never be deleted.
+ */
+ private static void truncateInternalProperties(String tableName, Statement ddlStatement, Connection connection) throws SQLException {
+ try (PreparedStatement preparedStatement = connection.prepareStatement("delete from internal_properties where kee not in (?,?)")) {
+ preparedStatement.setString(1, InternalProperties.DEFAULT_ORGANIZATION);
+ preparedStatement.setString(2, InternalProperties.SERVER_ID_CHECKSUM);
+ preparedStatement.execute();
+ // commit is useless on some databases
+ connection.commit();
+ }
+ }
+
+ /**
+ * Data in SCHEMA_MIGRATIONS table is inserted when DB is created and should not be altered afterwards.
+ */
+ private static void truncateSchemaMigrations(String tableName, Statement ddlStatement, Connection connection) {
+ // do nothing
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/ClusterVerification.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/ClusterVerification.java
new file mode 100644
index 00000000000..f8659270705
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/ClusterVerification.java
@@ -0,0 +1,58 @@
+/*
+ * 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 javax.annotation.Nullable;
+import org.sonar.api.Startable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.MessageException;
+
+@ServerSide
+public class ClusterVerification implements Startable {
+
+ private final WebServer server;
+ @Nullable
+ private final ClusterFeature feature;
+
+ public ClusterVerification(WebServer server, @Nullable ClusterFeature feature) {
+ this.server = server;
+ this.feature = feature;
+ }
+
+ public ClusterVerification(WebServer server) {
+ this(server, null);
+ }
+
+ @Override
+ public void start() {
+ if (server.isStandalone()) {
+ return;
+ }
+ if (feature == null || !feature.isEnabled()) {
+ throw MessageException.of(
+ "Cluster mode can't be enabled. Please install the Data Center Edition. More details at https://redirect.sonarsource.com/editions/datacenter.html.");
+ }
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DatabaseServerCompatibility.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DatabaseServerCompatibility.java
new file mode 100644
index 00000000000..ea8a7f7e336
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DatabaseServerCompatibility.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.platform;
+
+import java.util.Optional;
+import org.picocontainer.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.process.ProcessProperties;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+
+import static org.sonar.server.log.ServerProcessLogging.STARTUP_LOGGER_NAME;
+
+public class DatabaseServerCompatibility implements Startable {
+
+ private static final String HIGHLIGHTER = "################################################################################";
+
+ private final DatabaseVersion version;
+ private final Configuration configuration;
+
+ public DatabaseServerCompatibility(DatabaseVersion version, Configuration configuration) {
+ this.version = version;
+ this.configuration = configuration;
+ }
+
+ @Override
+ public void start() {
+ DatabaseVersion.Status status = version.getStatus();
+ if (status == DatabaseVersion.Status.REQUIRES_DOWNGRADE) {
+ throw MessageException.of("Database was upgraded to a more recent version of SonarQube. "
+ + "A backup must probably be restored or the DB settings are incorrect.");
+ }
+ if (status == DatabaseVersion.Status.REQUIRES_UPGRADE) {
+ Optional<Long> currentVersion = this.version.getVersion();
+ if (currentVersion.isPresent() && currentVersion.get() < DatabaseVersion.MIN_UPGRADE_VERSION) {
+ throw MessageException.of("Current version is too old. Please upgrade to Long Term Support version firstly.");
+ }
+ boolean blueGreen = configuration.getBoolean(ProcessProperties.Property.BLUE_GREEN_ENABLED.getKey()).orElse(false);
+ if (!blueGreen) {
+ String msg = "The database must be manually upgraded. Please backup the database and browse /setup. "
+ + "For more information: https://docs.sonarqube.org/latest/setup/upgrading";
+ Loggers.get(DatabaseServerCompatibility.class).warn(msg);
+ Loggers.get(STARTUP_LOGGER_NAME).warn('\n'
+ + HIGHLIGHTER + '\n'
+ + " " + msg
+ + '\n' + HIGHLIGHTER);
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ // do nothing
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DefaultServerUpgradeStatus.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DefaultServerUpgradeStatus.java
new file mode 100644
index 00000000000..6948c10225b
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DefaultServerUpgradeStatus.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.platform;
+
+import java.util.Optional;
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.picocontainer.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.platform.ServerUpgradeStatus;
+import org.sonar.process.ProcessProperties;
+import org.sonar.server.platform.db.migration.step.MigrationSteps;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+
+public class DefaultServerUpgradeStatus implements ServerUpgradeStatus, Startable {
+
+ private final DatabaseVersion dbVersion;
+ private final MigrationSteps migrationSteps;
+ private final Configuration configuration;
+
+ // available when connected to db
+ private long initialDbVersion;
+
+ public DefaultServerUpgradeStatus(DatabaseVersion dbVersion, MigrationSteps migrationSteps, Configuration configuration) {
+ this.dbVersion = dbVersion;
+ this.migrationSteps = migrationSteps;
+ this.configuration = configuration;
+ }
+
+ @Override
+ public void start() {
+ Optional<Long> v = dbVersion.getVersion();
+ this.initialDbVersion = v.orElse(-1L);
+ }
+
+ @Override
+ public void stop() {
+ // do nothing
+ }
+
+ @Override
+ public boolean isUpgraded() {
+ return !isFreshInstall() && (initialDbVersion < migrationSteps.getMaxMigrationNumber());
+ }
+
+ @Override
+ public boolean isFreshInstall() {
+ return initialDbVersion < 0;
+ }
+
+ @Override
+ public int getInitialDbVersion() {
+ return (int) initialDbVersion;
+ }
+
+ public boolean isBlueGreen() {
+ return configuration.getBoolean(ProcessProperties.Property.BLUE_GREEN_ENABLED.getKey()).orElse(false);
+ }
+
+ @Override
+ public String toString() {
+ return new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).toString();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/LogServerVersion.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/LogServerVersion.java
new file mode 100644
index 00000000000..ed78bd44f2b
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/LogServerVersion.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.platform;
+
+import com.google.common.base.Joiner;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.Startable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.Version;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+@ServerSide
+public class LogServerVersion implements Startable {
+
+ private static final Logger LOG = Loggers.get(LogServerVersion.class);
+ private final SonarRuntime runtime;
+
+ public LogServerVersion(SonarRuntime runtime) {
+ this.runtime = runtime;
+ }
+
+ @Override
+ public void start() {
+ String scmRevision = read("/build.properties").getProperty("Implementation-Build");
+ Version version = runtime.getApiVersion();
+ LOG.info("SonarQube {}", Joiner.on(" / ").skipNulls().join("Server", version, scmRevision));
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+
+ private static Properties read(String filePath) {
+ try (InputStream stream = LogServerVersion.class.getResourceAsStream(filePath)) {
+ Properties properties = new Properties();
+ properties.load(stream);
+ return properties;
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to read file " + filePath + " from classpath", e);
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/PersistentSettings.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/PersistentSettings.java
new file mode 100644
index 00000000000..818e13930c3
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/PersistentSettings.java
@@ -0,0 +1,84 @@
+/*
+ * 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 javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.config.Settings;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.setting.SettingsChangeNotifier;
+
+public class PersistentSettings {
+
+ private final Settings delegate;
+ private final DbClient dbClient;
+ private final SettingsChangeNotifier changeNotifier;
+
+ public PersistentSettings(Settings delegate, DbClient dbClient, SettingsChangeNotifier changeNotifier) {
+ this.delegate = delegate;
+ this.dbClient = dbClient;
+ this.changeNotifier = changeNotifier;
+ }
+
+ @CheckForNull
+ public String getString(String key) {
+ return delegate.getString(key);
+ }
+
+ /**
+ * Insert property into database if value is not {@code null}, else delete property from
+ * database. Session is not committed but {@link org.sonar.api.config.GlobalPropertyChangeHandler}
+ * are executed.
+ */
+ public PersistentSettings saveProperty(DbSession dbSession, String key, @Nullable String value) {
+ savePropertyImpl(dbSession, key, value);
+ changeNotifier.onGlobalPropertyChange(key, value);
+ return this;
+ }
+
+ /**
+ * Same as {@link #saveProperty(DbSession, String, String)} but a new database session is
+ * opened and committed.
+ */
+ public PersistentSettings saveProperty(String key, @Nullable String value) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ savePropertyImpl(dbSession, key, value);
+ dbSession.commit();
+ changeNotifier.onGlobalPropertyChange(key, value);
+ return this;
+ }
+ }
+
+ private void savePropertyImpl(DbSession dbSession, String key, @Nullable String value) {
+ if (value == null) {
+ dbClient.propertiesDao().deleteGlobalProperty(key, dbSession);
+ } else {
+ dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(key).setValue(value));
+ }
+ // refresh the cache of settings
+ delegate.setProperty(key, value);
+ }
+
+ public Settings getSettings() {
+ return delegate;
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/StartupMetadataPersister.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/StartupMetadataPersister.java
new file mode 100644
index 00000000000..41fa223b2f2
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/StartupMetadataPersister.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.platform;
+
+import java.util.Date;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.Startable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.db.DbClient;
+import org.sonar.db.property.PropertyDto;
+
+/**
+ * The server node marked as "startup leader" generates some information about startup. These
+ * information are loaded by "startup follower" servers and all Compute Engine nodes.
+ *
+ * @see StartupMetadataProvider#load(DbClient)
+ */
+@ServerSide
+public class StartupMetadataPersister implements Startable {
+
+ private final StartupMetadata metadata;
+ // PersistentSettings can not be used as it has to be
+ // instantiated in level 4 of container, whereas
+ // StartupMetadataPersister is level 3.
+ private final DbClient dbClient;
+
+ public StartupMetadataPersister(StartupMetadata metadata, DbClient dbClient) {
+ this.metadata = metadata;
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public void start() {
+ String startedAt = DateUtils.formatDateTime(new Date(metadata.getStartedAt()));
+ save(CoreProperties.SERVER_STARTTIME, startedAt);
+ }
+
+ private void save(String key, String value) {
+ dbClient.propertiesDao().saveProperty(new PropertyDto().setKey(key).setValue(value));
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java
new file mode 100644
index 00000000000..26f2df7e52f
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.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.platform;
+
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.extension.CoreExtensionRepository;
+import org.sonar.core.extension.CoreExtensionsInstaller;
+
+@ServerSide
+public class WebCoreExtensionsInstaller extends CoreExtensionsInstaller {
+ public WebCoreExtensionsInstaller(SonarRuntime sonarRuntime, CoreExtensionRepository coreExtensionRepository) {
+ super(sonarRuntime, coreExtensionRepository, ServerSide.class);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartup.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartup.java
new file mode 100644
index 00000000000..345e3dbc1c3
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartup.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.platform.db;
+
+import org.picocontainer.Startable;
+import org.sonar.api.platform.ServerUpgradeStatus;
+import org.sonar.server.platform.db.migration.charset.DatabaseCharsetChecker;
+
+/**
+ * Checks charset of all existing database columns at startup, before executing db migrations. This requires
+ * to be defined in platform level 2 ({@link org.sonar.server.platform.platformlevel.PlatformLevel2}).
+ */
+public class CheckDatabaseCharsetAtStartup implements Startable {
+
+ private final ServerUpgradeStatus upgradeStatus;
+ private final DatabaseCharsetChecker charsetChecker;
+
+ public CheckDatabaseCharsetAtStartup(ServerUpgradeStatus upgradeStatus, DatabaseCharsetChecker charsetChecker) {
+ this.upgradeStatus = upgradeStatus;
+ this.charsetChecker = charsetChecker;
+ }
+
+ @Override
+ public void start() {
+ DatabaseCharsetChecker.State state = DatabaseCharsetChecker.State.STARTUP;
+ if (upgradeStatus.isUpgraded()) {
+ state = DatabaseCharsetChecker.State.UPGRADE;
+ } else if (upgradeStatus.isFreshInstall()) {
+ state = DatabaseCharsetChecker.State.FRESH_INSTALL;
+ }
+ charsetChecker.check(state);
+ }
+
+ @Override
+ public void stop() {
+ // do nothing
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabase.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabase.java
new file mode 100644
index 00000000000..6cb54bb6e51
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabase.java
@@ -0,0 +1,116 @@
+/*
+ * 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.db;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import org.h2.Driver;
+import org.h2.tools.Server;
+import org.picocontainer.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+import static org.sonar.process.ProcessProperties.Property.JDBC_EMBEDDED_PORT;
+import static org.sonar.process.ProcessProperties.Property.JDBC_PASSWORD;
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+import static org.sonar.process.ProcessProperties.Property.JDBC_USERNAME;
+import static org.sonar.process.ProcessProperties.Property.PATH_DATA;
+
+public class EmbeddedDatabase implements Startable {
+ private static final Logger LOG = Loggers.get(EmbeddedDatabase.class);
+
+ private final Configuration config;
+ private final System2 system2;
+ private Server server;
+
+ public EmbeddedDatabase(Configuration config, System2 system2) {
+ this.config = config;
+ this.system2 = system2;
+ }
+
+ @Override
+ public void start() {
+ File dbHome = new File(getRequiredSetting(PATH_DATA.getKey()));
+ if (!dbHome.exists()) {
+ dbHome.mkdirs();
+ }
+
+ startServer(dbHome);
+ }
+
+ private void startServer(File dbHome) {
+ String url = getRequiredSetting(JDBC_URL.getKey());
+ String port = getRequiredSetting(JDBC_EMBEDDED_PORT.getKey());
+ String user = getSetting(JDBC_USERNAME.getKey());
+ String password = getSetting(JDBC_PASSWORD.getKey());
+ try {
+ // Db is used only by web server and compute engine. No need
+ // to make it accessible from outside.
+ system2.setProperty("h2.bindAddress", InetAddress.getLoopbackAddress().getHostAddress());
+
+ if (url.contains("/mem:")) {
+ server = Server.createTcpServer("-tcpPort", port, "-baseDir", dbHome.getAbsolutePath());
+ } else {
+ createDatabase(dbHome, user, password);
+ server = Server.createTcpServer("-tcpPort", port, "-ifExists", "-baseDir", dbHome.getAbsolutePath());
+ }
+
+ LOG.info("Starting embedded database on port " + server.getPort() + " with url " + url);
+ server.start();
+
+ LOG.info("Embedded database started. Data stored in: " + dbHome.getAbsolutePath());
+ } catch (SQLException e) {
+ throw new IllegalStateException("Unable to start database", e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (server != null) {
+ server.stop();
+ server = null;
+ LOG.info("Embedded database stopped");
+ }
+ }
+
+ private String getRequiredSetting(String property) {
+ String value = config.get(property).orElse("");
+ checkArgument(isNotEmpty(value), "Missing property %s", property);
+ return value;
+ }
+
+ private String getSetting(String name) {
+ return config.get(name).orElse("");
+ }
+
+ private static void createDatabase(File dbHome, String user, String password) throws SQLException {
+ String url = format("jdbc:h2:%s/sonar;USER=%s;PASSWORD=%s", dbHome.getAbsolutePath(), user, password);
+
+ DriverManager.registerDriver(new Driver());
+ DriverManager.getConnection(url).close();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabaseFactory.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabaseFactory.java
new file mode 100644
index 00000000000..9ed180873d3
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabaseFactory.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.platform.db;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.picocontainer.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.System2;
+
+import static org.apache.commons.lang.StringUtils.startsWith;
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+
+public class EmbeddedDatabaseFactory implements Startable {
+
+ private static final String URL_PREFIX = "jdbc:h2:tcp:";
+
+ private final Configuration config;
+ private final System2 system2;
+ private EmbeddedDatabase embeddedDatabase;
+
+ public EmbeddedDatabaseFactory(Configuration config, System2 system2) {
+ this.config = config;
+ this.system2 = system2;
+ }
+
+ @Override
+ public void start() {
+ if (embeddedDatabase == null) {
+ String jdbcUrl = config.get(JDBC_URL.getKey()).get();
+ if (startsWith(jdbcUrl, URL_PREFIX)) {
+ embeddedDatabase = createEmbeddedDatabase();
+ embeddedDatabase.start();
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (embeddedDatabase != null) {
+ embeddedDatabase.stop();
+ embeddedDatabase = null;
+ }
+ }
+
+ @VisibleForTesting
+ EmbeddedDatabase createEmbeddedDatabase() {
+ return new EmbeddedDatabase(config, system2);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java
new file mode 100644
index 00000000000..7327cacb0f2
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java
@@ -0,0 +1,52 @@
+/*
+ * 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.db.migration;
+
+import org.picocontainer.Startable;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.server.platform.DefaultServerUpgradeStatus;
+import org.sonar.server.platform.db.migration.engine.MigrationEngine;
+
+public class AutoDbMigration implements Startable {
+ private final DefaultServerUpgradeStatus serverUpgradeStatus;
+ private final MigrationEngine migrationEngine;
+
+ public AutoDbMigration(DefaultServerUpgradeStatus serverUpgradeStatus, MigrationEngine migrationEngine) {
+ this.serverUpgradeStatus = serverUpgradeStatus;
+ this.migrationEngine = migrationEngine;
+ }
+
+ @Override
+ public void start() {
+ if (serverUpgradeStatus.isFreshInstall()) {
+ Loggers.get(getClass()).info("Automatically perform DB migration on fresh install");
+ migrationEngine.execute();
+ } else if (serverUpgradeStatus.isUpgraded() && serverUpgradeStatus.isBlueGreen()) {
+ Loggers.get(getClass()).info("Automatically perform DB migration on blue/green deployment");
+ migrationEngine.execute();
+ }
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorService.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorService.java
new file mode 100644
index 00000000000..63306e38ac8
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorService.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.platform.db.migration;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Flag interface for the ExecutorService to be used by the {@link DatabaseMigrationImpl}
+ * component.
+ */
+public interface DatabaseMigrationExecutorService extends ExecutorService {
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceImpl.java
new file mode 100644
index 00000000000..659b8fbcd10
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceImpl.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.platform.db.migration;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.sonar.server.util.AbstractStoppableExecutorService;
+
+import java.util.concurrent.Executors;
+
+/**
+ * Since only one DB migration can run at a time, this implementation of DatabaseMigrationExecutorService
+ * wraps a single thread executor from the JDK.
+ */
+public class DatabaseMigrationExecutorServiceImpl
+ extends AbstractStoppableExecutorService
+ implements DatabaseMigrationExecutorService {
+
+ public DatabaseMigrationExecutorServiceImpl() {
+ super(
+ Executors.newSingleThreadExecutor(
+ new ThreadFactoryBuilder()
+ .setDaemon(false)
+ .setNameFormat("DB_migration-%d")
+ .build()
+ ));
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java
new file mode 100644
index 00000000000..7e5d5ba359f
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java
@@ -0,0 +1,116 @@
+/*
+ * 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.db.migration;
+
+import java.util.Date;
+import java.util.concurrent.Semaphore;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.util.logs.Profiler;
+import org.sonar.server.platform.Platform;
+import org.sonar.server.platform.db.migration.DatabaseMigrationState.Status;
+import org.sonar.server.platform.db.migration.engine.MigrationEngine;
+import org.sonar.server.platform.db.migration.step.MigrationStepExecutionException;
+
+/**
+ * Handles concurrency to make sure only one DB migration can run at a time.
+ */
+public class DatabaseMigrationImpl implements DatabaseMigration {
+
+ private static final Logger LOGGER = Loggers.get(DatabaseMigrationImpl.class);
+
+ /**
+ * ExecutorService implements threads management.
+ */
+ private final DatabaseMigrationExecutorService executorService;
+ private final MigrationEngine migrationEngine;
+ private final Platform platform;
+ private final MutableDatabaseMigrationState migrationState;
+ /**
+ * This semaphore implements thread safety from concurrent calls of method {@link #startIt()}
+ */
+ private final Semaphore semaphore = new Semaphore(1);
+
+ public DatabaseMigrationImpl(DatabaseMigrationExecutorService executorService, MutableDatabaseMigrationState migrationState,
+ MigrationEngine migrationEngine, Platform platform) {
+ this.executorService = executorService;
+ this.migrationState = migrationState;
+ this.migrationEngine = migrationEngine;
+ this.platform = platform;
+ }
+
+ @Override
+ public void startIt() {
+ if (semaphore.tryAcquire()) {
+ try {
+ executorService.execute(this::doDatabaseMigration);
+ } catch (RuntimeException e) {
+ semaphore.release();
+ throw e;
+ }
+ } else {
+ LOGGER.trace("{}: lock is already taken or process is already running", Thread.currentThread().getName());
+ }
+ }
+
+ private void doDatabaseMigration() {
+ migrationState.setStatus(Status.RUNNING);
+ migrationState.setStartedAt(new Date());
+ migrationState.setError(null);
+ Profiler profiler = Profiler.create(LOGGER);
+ try {
+ profiler.startInfo("Starting DB Migration and container restart");
+ doUpgradeDb();
+ doRestartContainer();
+ migrationState.setStatus(Status.SUCCEEDED);
+ profiler.stopInfo("DB Migration and container restart: success");
+ } catch (MigrationStepExecutionException e) {
+ profiler.stopError("DB migration failed");
+ LOGGER.error("DB migration ended with an exception", e);
+ saveStatus(e);
+ } catch (Throwable t) {
+ profiler.stopError("Container restart failed");
+ LOGGER.error("Container restart failed", t);
+ saveStatus(t);
+ } finally {
+ semaphore.release();
+ }
+ }
+
+ private void saveStatus(Throwable e) {
+ migrationState.setStatus(Status.FAILED);
+ migrationState.setError(e);
+ }
+
+ private void doUpgradeDb() {
+ Profiler profiler = Profiler.createIfTrace(LOGGER);
+ profiler.startTrace("Starting DB Migration");
+ migrationEngine.execute();
+ profiler.stopTrace("DB Migration ended");
+ }
+
+ private void doRestartContainer() {
+ Profiler profiler = Profiler.createIfTrace(LOGGER);
+ profiler.startTrace("Restarting container");
+ platform.doStart();
+ profiler.stopTrace("Container restarted successfully");
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/package-info.java
new file mode 100644
index 00000000000..52a8464fb32
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/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.db.migration;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/package-info.java
new file mode 100644
index 00000000000..de34ff5428d
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/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.db;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/BaseSectionMBean.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/BaseSectionMBean.java
new file mode 100644
index 00000000000..c7ff804adf3
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/BaseSectionMBean.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.platform.monitoring;
+
+import org.picocontainer.Startable;
+import org.sonar.process.Jmx;
+import org.sonar.process.systeminfo.SystemInfoSection;
+
+/**
+ * Base implementation of a {@link SystemInfoSection}
+ * that is exported as a JMX bean
+ */
+public abstract class BaseSectionMBean implements SystemInfoSection, Startable {
+
+ /**
+ * Auto-registers to MBean server
+ */
+ @Override
+ public void start() {
+ Jmx.register(objectName(), this);
+ }
+
+ /**
+ * Unregister, if needed
+ */
+ @Override
+ public void stop() {
+ Jmx.unregister(objectName());
+ }
+
+ String objectName() {
+ return "SonarQube:name=" + name();
+ }
+
+ /**
+ * Name of section in System Info page
+ */
+ abstract String name();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSection.java
new file mode 100644
index 00000000000..871fbab5e62
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSection.java
@@ -0,0 +1,125 @@
+/*
+ * 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.monitoring;
+
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.db.DbClient;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo.Section;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+/**
+ * Information about database connection pool
+ */
+public class DbConnectionSection extends BaseSectionMBean implements DbConnectionSectionMBean {
+
+ private final DatabaseVersion dbVersion;
+ private final DbClient dbClient;
+ private final SonarRuntime runtime;
+
+ public DbConnectionSection(DatabaseVersion dbVersion, DbClient dbClient, SonarRuntime runtime) {
+ this.dbVersion = dbVersion;
+ this.dbClient = dbClient;
+ this.runtime = runtime;
+ }
+
+ @Override
+ public String name() {
+ return "Database";
+ }
+
+ @Override
+ public String getMigrationStatus() {
+ return dbVersion.getStatus().name();
+ }
+
+ @Override
+ public int getPoolActiveConnections() {
+ return commonsDbcp().getNumActive();
+ }
+
+ @Override
+ public int getPoolMaxActiveConnections() {
+ return commonsDbcp().getMaxTotal();
+ }
+
+ @Override
+ public int getPoolIdleConnections() {
+ return commonsDbcp().getNumIdle();
+ }
+
+ @Override
+ public int getPoolMaxIdleConnections() {
+ return commonsDbcp().getMaxIdle();
+ }
+
+ @Override
+ public int getPoolMinIdleConnections() {
+ return commonsDbcp().getMinIdle();
+ }
+
+ @Override
+ public int getPoolInitialSize() {
+ return commonsDbcp().getInitialSize();
+ }
+
+ @Override
+ public long getPoolMaxWaitMillis() {
+ return commonsDbcp().getMaxWaitMillis();
+ }
+
+ @Override
+ public boolean getPoolRemoveAbandoned() {
+ return commonsDbcp().getRemoveAbandonedOnBorrow();
+ }
+
+ @Override
+ public int getPoolRemoveAbandonedTimeoutSeconds() {
+ return commonsDbcp().getRemoveAbandonedTimeout();
+ }
+
+ @Override
+ public Section toProtobuf() {
+ Section.Builder protobuf = Section.newBuilder();
+ String side = runtime.getSonarQubeSide() == SonarQubeSide.COMPUTE_ENGINE ? "Compute Engine" : "Web";
+ protobuf.setName(side + " Database Connection");
+ completePoolAttributes(protobuf);
+ return protobuf.build();
+ }
+
+ private void completePoolAttributes(Section.Builder protobuf) {
+ setAttribute(protobuf, "Pool Active Connections", getPoolActiveConnections());
+ setAttribute(protobuf, "Pool Max Connections", getPoolMaxActiveConnections());
+ setAttribute(protobuf, "Pool Initial Size", getPoolInitialSize());
+ setAttribute(protobuf, "Pool Idle Connections", getPoolIdleConnections());
+ setAttribute(protobuf, "Pool Min Idle Connections", getPoolMinIdleConnections());
+ setAttribute(protobuf, "Pool Max Idle Connections", getPoolMaxIdleConnections());
+ setAttribute(protobuf, "Pool Max Wait (ms)", getPoolMaxWaitMillis());
+ setAttribute(protobuf, "Pool Remove Abandoned", getPoolRemoveAbandoned());
+ setAttribute(protobuf, "Pool Remove Abandoned Timeout (seconds)", getPoolRemoveAbandonedTimeoutSeconds());
+ }
+
+ private BasicDataSource commonsDbcp() {
+ return (BasicDataSource) dbClient.getDatabase().getDataSource();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSectionMBean.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSectionMBean.java
new file mode 100644
index 00000000000..8aca3210e84
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSectionMBean.java
@@ -0,0 +1,71 @@
+/*
+ * 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.monitoring;
+
+public interface DbConnectionSectionMBean {
+
+ /**
+ * Is database schema up-to-date or should it be upgraded ?
+ */
+ String getMigrationStatus();
+
+ /**
+ *
+ */
+ int getPoolActiveConnections();
+
+ /**
+ * The maximum number of active connections that can be allocated from this pool at the same time, or negative for no limit.
+ */
+ int getPoolMaxActiveConnections();
+
+ int getPoolIdleConnections();
+
+ /**
+ * The maximum number of connections that can remain idle in the pool, without extra ones being released, or negative for no limit.
+ */
+ int getPoolMaxIdleConnections();
+
+ /**
+ * The minimum number of connections that can remain idle in the pool, without extra ones being created, or zero to create none.
+ */
+ int getPoolMinIdleConnections();
+
+ /**
+ * The initial number of connections that are created when the pool is started.
+ */
+ int getPoolInitialSize();
+
+ /**
+ * The maximum number of milliseconds that the pool will wait
+ * (when there are no available connections) for a connection to be returned before throwing an exception, or -1 to wait indefinitely.
+ */
+ long getPoolMaxWaitMillis();
+
+ /**
+ * Flag to remove abandoned connections if they exceed the {@link #getPoolRemoveAbandonedTimeoutSeconds()}.
+ */
+ boolean getPoolRemoveAbandoned();
+
+ /**
+ * Timeout in seconds before an abandoned connection can be removed.
+ */
+ int getPoolRemoveAbandonedTimeoutSeconds();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsIndexesSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsIndexesSection.java
new file mode 100644
index 00000000000..1d07f5a32cd
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsIndexesSection.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.platform.monitoring;
+
+import java.util.Map;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.admin.indices.stats.IndexStats;
+import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.process.systeminfo.Global;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsClient;
+
+import static org.apache.commons.io.FileUtils.byteCountToDisplaySize;
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+@ServerSide
+public class EsIndexesSection implements SystemInfoSection, Global {
+
+ private final EsClient esClient;
+
+ public EsIndexesSection(EsClient esClient) {
+ this.esClient = esClient;
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("Search Indexes");
+ try {
+ completeIndexAttributes(protobuf);
+ } catch (Exception es) {
+ Loggers.get(EsIndexesSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"Error\" attribute.", es);
+ setAttribute(protobuf, "Error", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage());
+ }
+ return protobuf.build();
+ }
+
+ private void completeIndexAttributes(ProtobufSystemInfo.Section.Builder protobuf) {
+ IndicesStatsResponse indicesStats = esClient.prepareStats().all().get();
+ for (Map.Entry<String, IndexStats> indexStats : indicesStats.getIndices().entrySet()) {
+ String prefix = "Index " + indexStats.getKey() + " - ";
+ setAttribute(protobuf, prefix + "Docs", indexStats.getValue().getPrimaries().getDocs().getCount());
+ setAttribute(protobuf, prefix + "Shards", indexStats.getValue().getShards().length);
+ setAttribute(protobuf, prefix + "Store Size", byteCountToDisplaySize(indexStats.getValue().getPrimaries().getStore().getSizeInBytes()));
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java
new file mode 100644
index 00000000000..a865c7e9d0e
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java
@@ -0,0 +1,106 @@
+/*
+ * 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.monitoring;
+
+import java.util.Locale;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
+import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
+import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.common.breaker.CircuitBreaker;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsClient;
+
+import static java.lang.String.format;
+import static org.apache.commons.io.FileUtils.byteCountToDisplaySize;
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+public class EsStateSection implements SystemInfoSection {
+
+ private final EsClient esClient;
+
+ public EsStateSection(EsClient esClient) {
+ this.esClient = esClient;
+ }
+
+ private ClusterHealthStatus getStateAsEnum() {
+ return clusterStats().getStatus();
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("Search State");
+ try {
+ setAttribute(protobuf, "State", getStateAsEnum().name());
+ completeNodeAttributes(protobuf);
+ } catch (Exception es) {
+ Loggers.get(EsStateSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"state\" attribute.", es);
+ setAttribute(protobuf, "State", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage());
+ }
+ return protobuf.build();
+ }
+
+ private void completeNodeAttributes(ProtobufSystemInfo.Section.Builder protobuf) {
+ NodesStatsResponse nodesStats = esClient.prepareNodesStats()
+ .setFs(true)
+ .setProcess(true)
+ .setJvm(true)
+ .setIndices(true)
+ .setBreaker(true)
+ .get();
+ if (!nodesStats.getNodes().isEmpty()) {
+ NodeStats stats = nodesStats.getNodes().get(0);
+ toProtobuf(stats, protobuf);
+ }
+ }
+
+ public static void toProtobuf(NodeStats stats, ProtobufSystemInfo.Section.Builder protobuf) {
+ setAttribute(protobuf, "CPU Usage (%)", stats.getProcess().getCpu().getPercent());
+ setAttribute(protobuf, "Disk Available", byteCountToDisplaySize(stats.getFs().getTotal().getAvailable().getBytes()));
+ setAttribute(protobuf, "Store Size", byteCountToDisplaySize(stats.getIndices().getStore().getSizeInBytes()));
+ setAttribute(protobuf, "Translog Size", byteCountToDisplaySize(stats.getIndices().getTranslog().getTranslogSizeInBytes()));
+ setAttribute(protobuf, "Open File Descriptors", stats.getProcess().getOpenFileDescriptors());
+ setAttribute(protobuf, "Max File Descriptors", stats.getProcess().getMaxFileDescriptors());
+ setAttribute(protobuf, "JVM Heap Usage", formatPercent(stats.getJvm().getMem().getHeapUsedPercent()));
+ setAttribute(protobuf, "JVM Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getHeapUsed().getBytes()));
+ setAttribute(protobuf, "JVM Heap Max", byteCountToDisplaySize(stats.getJvm().getMem().getHeapMax().getBytes()));
+ setAttribute(protobuf, "JVM Non Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getNonHeapUsed().getBytes()));
+ setAttribute(protobuf, "JVM Threads", stats.getJvm().getThreads().getCount());
+ setAttribute(protobuf, "Field Data Memory", byteCountToDisplaySize(stats.getIndices().getFieldData().getMemorySizeInBytes()));
+ setAttribute(protobuf, "Field Data Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getLimit()));
+ setAttribute(protobuf, "Field Data Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getEstimated()));
+ setAttribute(protobuf, "Request Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getLimit()));
+ setAttribute(protobuf, "Request Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getEstimated()));
+ setAttribute(protobuf, "Query Cache Memory", byteCountToDisplaySize(stats.getIndices().getQueryCache().getMemorySizeInBytes()));
+ setAttribute(protobuf, "Request Cache Memory", byteCountToDisplaySize(stats.getIndices().getRequestCache().getMemorySizeInBytes()));
+ }
+
+ private ClusterStatsResponse clusterStats() {
+ return esClient.prepareClusterStats().get();
+ }
+
+ private static String formatPercent(long amount) {
+ return format(Locale.ENGLISH, "%.1f%%", 100.0 * amount * 1.0 / 100.0);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/PluginsSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/PluginsSection.java
new file mode 100644
index 00000000000..4fd9bb19942
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/PluginsSection.java
@@ -0,0 +1,53 @@
+/*
+ * 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.monitoring;
+
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginRepository;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.updatecenter.common.Version;
+
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+@ServerSide
+public class PluginsSection implements SystemInfoSection {
+ private final PluginRepository repository;
+
+ public PluginsSection(PluginRepository repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("Plugins");
+ for (PluginInfo plugin : repository.getPluginInfos()) {
+ String label = "[" + plugin.getName() + "]";
+ Version version = plugin.getVersion();
+ if (version != null) {
+ label = version.getName() + " " + label;
+ }
+ setAttribute(protobuf, plugin.getKey(), label);
+ }
+ return protobuf.build();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SettingsSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SettingsSection.java
new file mode 100644
index 00000000000..daa26a5e6f7
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SettingsSection.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.platform.monitoring;
+
+import java.util.Map;
+import java.util.TreeMap;
+import org.sonar.api.PropertyType;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.server.ServerSide;
+import org.sonar.process.systeminfo.Global;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+import static org.apache.commons.lang.StringUtils.abbreviate;
+import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
+import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
+import static org.sonar.process.ProcessProperties.Property.AUTH_JWT_SECRET;
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+@ServerSide
+public class SettingsSection implements SystemInfoSection, Global {
+
+ private static final int MAX_VALUE_LENGTH = 500;
+ private static final String PASSWORD_VALUE = "xxxxxxxx";
+ private final Settings settings;
+
+ public SettingsSection(Settings settings) {
+ this.settings = settings;
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("Settings");
+
+ PropertyDefinitions definitions = settings.getDefinitions();
+ TreeMap<String, String> orderedProps = new TreeMap<>(settings.getProperties());
+ for (Map.Entry<String, String> prop : orderedProps.entrySet()) {
+ String key = prop.getKey();
+ String value = obfuscateValue(definitions, key, prop.getValue());
+ setAttribute(protobuf, key, value);
+ }
+ return protobuf.build();
+ }
+
+ private static String obfuscateValue(PropertyDefinitions definitions, String key, String value) {
+ PropertyDefinition def = definitions.get(key);
+ if (def != null && def.type() == PropertyType.PASSWORD) {
+ return PASSWORD_VALUE;
+ }
+ if (endsWithIgnoreCase(key, ".secured") ||
+ containsIgnoreCase(key, "password") ||
+ containsIgnoreCase(key, "passcode") ||
+ AUTH_JWT_SECRET.getKey().equals(key)) {
+ return PASSWORD_VALUE;
+ }
+ return abbreviate(value, MAX_VALUE_LENGTH);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/StandaloneSystemSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/StandaloneSystemSection.java
new file mode 100644
index 00000000000..1454ae298b0
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/StandaloneSystemSection.java
@@ -0,0 +1,138 @@
+/*
+ * 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.monitoring;
+
+import com.google.common.base.Joiner;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.platform.Server;
+import org.sonar.api.security.SecurityRealm;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.authentication.IdentityProviderRepository;
+import org.sonar.server.log.ServerLogging;
+import org.sonar.server.platform.OfficialDistribution;
+import org.sonar.server.user.SecurityRealmFactory;
+
+import static org.sonar.process.ProcessProperties.Property.PATH_DATA;
+import static org.sonar.process.ProcessProperties.Property.PATH_HOME;
+import static org.sonar.process.ProcessProperties.Property.PATH_TEMP;
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+public class StandaloneSystemSection extends BaseSectionMBean implements SystemSectionMBean {
+
+ private static final Joiner COMMA_JOINER = Joiner.on(", ");
+
+ private final Configuration config;
+ private final SecurityRealmFactory securityRealmFactory;
+ private final IdentityProviderRepository identityProviderRepository;
+ private final Server server;
+ private final ServerLogging serverLogging;
+ private final OfficialDistribution officialDistribution;
+
+ public StandaloneSystemSection(Configuration config, SecurityRealmFactory securityRealmFactory,
+ IdentityProviderRepository identityProviderRepository, Server server, ServerLogging serverLogging,
+ OfficialDistribution officialDistribution) {
+ this.config = config;
+ this.securityRealmFactory = securityRealmFactory;
+ this.identityProviderRepository = identityProviderRepository;
+ this.server = server;
+ this.serverLogging = serverLogging;
+ this.officialDistribution = officialDistribution;
+ }
+
+ @Override
+ public String getServerId() {
+ return server.getId();
+ }
+
+ @Override
+ public String getVersion() {
+ return server.getVersion();
+ }
+
+ @Override
+ public String getLogLevel() {
+ return serverLogging.getRootLoggerLevel().name();
+ }
+
+ @CheckForNull
+ private String getExternalUserAuthentication() {
+ SecurityRealm realm = securityRealmFactory.getRealm();
+ return realm == null ? null : realm.getName();
+ }
+
+ private List<String> getEnabledIdentityProviders() {
+ return identityProviderRepository.getAllEnabledAndSorted()
+ .stream()
+ .filter(IdentityProvider::isEnabled)
+ .map(IdentityProvider::getName)
+ .collect(MoreCollectors.toList());
+ }
+
+ private List<String> getAllowsToSignUpEnabledIdentityProviders() {
+ return identityProviderRepository.getAllEnabledAndSorted()
+ .stream()
+ .filter(IdentityProvider::isEnabled)
+ .filter(IdentityProvider::allowsUsersToSignUp)
+ .map(IdentityProvider::getName)
+ .collect(MoreCollectors.toList());
+ }
+
+ private boolean getForceAuthentication() {
+ return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false);
+ }
+
+ @Override
+ public String name() {
+ // JMX name
+ return "SonarQube";
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("System");
+
+ setAttribute(protobuf, "Server ID", server.getId());
+ setAttribute(protobuf, "Version", getVersion());
+ setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication());
+ addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders());
+ addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders());
+ setAttribute(protobuf, "High Availability", false);
+ setAttribute(protobuf, "Official Distribution", officialDistribution.check());
+ setAttribute(protobuf, "Force authentication", getForceAuthentication());
+ setAttribute(protobuf, "Home Dir", config.get(PATH_HOME.getKey()).orElse(null));
+ setAttribute(protobuf, "Data Dir", config.get(PATH_DATA.getKey()).orElse(null));
+ setAttribute(protobuf, "Temp Dir", config.get(PATH_TEMP.getKey()).orElse(null));
+ setAttribute(protobuf, "Processors", Runtime.getRuntime().availableProcessors());
+ return protobuf.build();
+ }
+
+ private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) {
+ if (values != null && !values.isEmpty()) {
+ setAttribute(protobuf, key, COMMA_JOINER.join(values));
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java
new file mode 100644
index 00000000000..07cc22074c0
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.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.monitoring;
+
+import javax.annotation.CheckForNull;
+
+public interface SystemSectionMBean {
+ @CheckForNull
+ String getServerId();
+
+ String getVersion();
+
+ String getLogLevel();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoader.java
new file mode 100644
index 00000000000..cea4044190b
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoader.java
@@ -0,0 +1,27 @@
+/*
+ * 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.monitoring.cluster;
+
+import java.util.Collection;
+
+public interface AppNodesInfoLoader {
+
+ Collection<NodeInfo> load() throws InterruptedException;
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImpl.java
new file mode 100644
index 00000000000..3fdccddf5e9
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImpl.java
@@ -0,0 +1,82 @@
+/*
+ * 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.monitoring.cluster;
+
+import com.hazelcast.core.Member;
+import com.hazelcast.core.MemberSelector;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.api.server.ServerSide;
+import org.sonar.process.ProcessId;
+import org.sonar.process.cluster.hz.DistributedAnswer;
+import org.sonar.process.cluster.hz.HazelcastMember;
+import org.sonar.process.cluster.hz.HazelcastMemberSelectors;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+import static org.sonar.process.cluster.hz.HazelcastMember.Attribute.NODE_NAME;
+
+@ServerSide
+public class AppNodesInfoLoaderImpl implements AppNodesInfoLoader {
+
+ /**
+ * Timeout to get information from all nodes
+ */
+ private static final long DISTRIBUTED_TIMEOUT_MS = 15_000L;
+
+ private final HazelcastMember hzMember;
+
+ public AppNodesInfoLoaderImpl(HazelcastMember hzMember) {
+ this.hzMember = hzMember;
+ }
+
+ public AppNodesInfoLoaderImpl() {
+ this(null);
+ }
+
+ public Collection<NodeInfo> load() throws InterruptedException {
+ Map<String, NodeInfo> nodesByName = new HashMap<>();
+ MemberSelector memberSelector = HazelcastMemberSelectors.selectorForProcessIds(ProcessId.WEB_SERVER, ProcessId.COMPUTE_ENGINE);
+ DistributedAnswer<ProtobufSystemInfo.SystemInfo> distributedAnswer = hzMember.call(ProcessInfoProvider::provide, memberSelector, DISTRIBUTED_TIMEOUT_MS);
+ for (Member member : distributedAnswer.getMembers()) {
+ String nodeName = member.getStringAttribute(NODE_NAME.getKey());
+ NodeInfo nodeInfo = nodesByName.computeIfAbsent(nodeName, name -> {
+ NodeInfo info = new NodeInfo(name);
+ info.setHost(member.getAddress().getHost());
+ return info;
+ });
+ completeNodeInfo(distributedAnswer, member, nodeInfo);
+ }
+ return nodesByName.values();
+ }
+
+ private static void completeNodeInfo(DistributedAnswer<ProtobufSystemInfo.SystemInfo> distributedAnswer, Member member, NodeInfo nodeInfo) {
+ Optional<ProtobufSystemInfo.SystemInfo> nodeAnswer = distributedAnswer.getAnswer(member);
+ Optional<Exception> failure = distributedAnswer.getFailed(member);
+ if (distributedAnswer.hasTimedOut(member)) {
+ nodeInfo.setErrorMessage("Failed to retrieve information on time");
+ } else if (failure.isPresent()) {
+ nodeInfo.setErrorMessage("Failed to retrieve information: " + failure.get().getMessage());
+ } else if (nodeAnswer.isPresent()) {
+ nodeAnswer.get().getSectionsList().forEach(nodeInfo::addSection);
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSection.java
new file mode 100644
index 00000000000..5d05864ea23
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSection.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.platform.monitoring.cluster;
+
+import javax.annotation.Nullable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.ce.configuration.WorkerCountProvider;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.ce.CeQueueDto;
+import org.sonar.process.systeminfo.Global;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.property.InternalProperties;
+
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+@ServerSide
+public class CeQueueGlobalSection implements SystemInfoSection, Global {
+
+ private static final int DEFAULT_NB_OF_WORKERS = 1;
+
+ private final DbClient dbClient;
+ @Nullable
+ private final WorkerCountProvider workerCountProvider;
+
+ public CeQueueGlobalSection(DbClient dbClient, @Nullable WorkerCountProvider workerCountProvider) {
+ this.dbClient = dbClient;
+ this.workerCountProvider = workerCountProvider;
+ }
+
+ public CeQueueGlobalSection(DbClient dbClient) {
+ this(dbClient, null);
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("Compute Engine Tasks");
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ setAttribute(protobuf, "Total Pending", dbClient.ceQueueDao().countByStatus(dbSession, CeQueueDto.Status.PENDING));
+ setAttribute(protobuf, "Total In Progress", dbClient.ceQueueDao().countByStatus(dbSession, CeQueueDto.Status.IN_PROGRESS));
+ setAttribute(protobuf, "Max Workers per Node", workerCountProvider == null ? DEFAULT_NB_OF_WORKERS : workerCountProvider.get());
+ setAttribute(protobuf, "Workers Paused", "true".equals(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.COMPUTE_ENGINE_PAUSE).orElse(null)));
+ }
+ return protobuf.build();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSection.java
new file mode 100644
index 00000000000..e257de37c9f
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSection.java
@@ -0,0 +1,54 @@
+/*
+ * 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.monitoring.cluster;
+
+import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse;
+import org.sonar.api.server.ServerSide;
+import org.sonar.process.systeminfo.Global;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsClient;
+
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+/**
+ * In cluster mode, section "Search" that displays all ES information
+ * that are not specific to a node or an index
+ */
+@ServerSide
+public class EsClusterStateSection implements SystemInfoSection, Global {
+
+ private final EsClient esClient;
+
+ public EsClusterStateSection(EsClient esClient) {
+ this.esClient = esClient;
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("Search State");
+ ClusterStatsResponse stats = esClient.prepareClusterStats().get();
+ setAttribute(protobuf, "State", stats.getStatus().name());
+ setAttribute(protobuf, "Nodes", stats.getNodesStats().getCounts().getTotal());
+ return protobuf.build();
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoader.java
new file mode 100644
index 00000000000..6cfde83913a
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoader.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.platform.monitoring.cluster;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.sonar.api.server.ServerSide;
+import org.sonar.process.systeminfo.Global;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+@ServerSide
+public class GlobalInfoLoader {
+ private final List<SystemInfoSection> globalSections;
+
+ public GlobalInfoLoader(SystemInfoSection[] sections) {
+ this.globalSections = Arrays.stream(sections)
+ .filter(section -> section instanceof Global)
+ .collect(Collectors.toList());
+ }
+
+ public List<ProtobufSystemInfo.Section> load() {
+ return globalSections.stream()
+ .map(SystemInfoSection::toProtobuf)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSection.java
new file mode 100644
index 00000000000..6ebb83d06f0
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSection.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.platform.monitoring.cluster;
+
+import com.google.common.base.Joiner;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.platform.Server;
+import org.sonar.api.security.SecurityRealm;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.process.systeminfo.Global;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.authentication.IdentityProviderRepository;
+import org.sonar.server.user.SecurityRealmFactory;
+
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+@ServerSide
+public class GlobalSystemSection implements SystemInfoSection, Global {
+ private static final Joiner COMMA_JOINER = Joiner.on(", ");
+
+ private final Configuration config;
+ private final Server server;
+ private final SecurityRealmFactory securityRealmFactory;
+ private final IdentityProviderRepository identityProviderRepository;
+
+ public GlobalSystemSection(Configuration config, Server server, SecurityRealmFactory securityRealmFactory,
+ IdentityProviderRepository identityProviderRepository) {
+ this.config = config;
+ this.server = server;
+ this.securityRealmFactory = securityRealmFactory;
+ this.identityProviderRepository = identityProviderRepository;
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("System");
+
+ setAttribute(protobuf, "Server ID", server.getId());
+ setAttribute(protobuf, "High Availability", true);
+ setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication());
+ addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders());
+ addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders());
+ setAttribute(protobuf, "Force authentication", getForceAuthentication());
+ return protobuf.build();
+ }
+
+ private List<String> getEnabledIdentityProviders() {
+ return identityProviderRepository.getAllEnabledAndSorted()
+ .stream()
+ .filter(IdentityProvider::isEnabled)
+ .map(IdentityProvider::getName)
+ .collect(MoreCollectors.toList());
+ }
+
+ private List<String> getAllowsToSignUpEnabledIdentityProviders() {
+ return identityProviderRepository.getAllEnabledAndSorted()
+ .stream()
+ .filter(IdentityProvider::isEnabled)
+ .filter(IdentityProvider::allowsUsersToSignUp)
+ .map(IdentityProvider::getName)
+ .collect(MoreCollectors.toList());
+ }
+
+ private boolean getForceAuthentication() {
+ return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false);
+ }
+
+ private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) {
+ if (values != null && !values.isEmpty()) {
+ setAttribute(protobuf, key, COMMA_JOINER.join(values));
+ }
+ }
+
+ @CheckForNull
+ private String getExternalUserAuthentication() {
+ SecurityRealm realm = securityRealmFactory.getRealm();
+ return realm == null ? null : realm.getName();
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeInfo.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeInfo.java
new file mode 100644
index 00000000000..d60be12979e
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeInfo.java
@@ -0,0 +1,99 @@
+/*
+ * 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.monitoring.cluster;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+/**
+ * Represents the system information of a cluster node. In the case of
+ * application node, it merges information from Web Server and Compute
+ * Engine processes.
+ *
+ */
+public class NodeInfo {
+
+ private final String name;
+ private String host = null;
+ private Long startedAt = null;
+ private String errorMessage = null;
+ private final List<ProtobufSystemInfo.Section> sections = new ArrayList<>();
+
+ public NodeInfo(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Optional<String> getHost() {
+ return Optional.ofNullable(host);
+ }
+
+ public void setHost(@Nullable String s) {
+ this.host = s;
+ }
+
+ public Optional<Long> getStartedAt() {
+ return Optional.ofNullable(startedAt);
+ }
+
+ public void setStartedAt(@Nullable Long l) {
+ this.startedAt = l;
+ }
+
+ public Optional<String> getErrorMessage() {
+ return Optional.ofNullable(errorMessage);
+ }
+
+ public void setErrorMessage(@Nullable String s) {
+ this.errorMessage = s;
+ }
+
+ public NodeInfo addSection(ProtobufSystemInfo.Section section) {
+ this.sections.add(section);
+ return this;
+ }
+
+ public List<ProtobufSystemInfo.Section> getSections() {
+ return sections;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ NodeInfo nodeInfo = (NodeInfo) o;
+ return name.equals(nodeInfo.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSection.java
new file mode 100644
index 00000000000..0960336bdaf
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSection.java
@@ -0,0 +1,61 @@
+/*
+ * 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.monitoring.cluster;
+
+import org.sonar.api.config.Configuration;
+import org.sonar.api.platform.Server;
+import org.sonar.api.server.ServerSide;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.platform.OfficialDistribution;
+
+import static org.sonar.process.ProcessProperties.Property.PATH_DATA;
+import static org.sonar.process.ProcessProperties.Property.PATH_HOME;
+import static org.sonar.process.ProcessProperties.Property.PATH_TEMP;
+import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;
+
+@ServerSide
+public class NodeSystemSection implements SystemInfoSection {
+
+ private final Configuration config;
+ private final Server server;
+ private final OfficialDistribution officialDistribution;
+
+ public NodeSystemSection(Configuration config, Server server, OfficialDistribution officialDistribution) {
+ this.config = config;
+ this.server = server;
+ this.officialDistribution = officialDistribution;
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
+ protobuf.setName("System");
+
+ setAttribute(protobuf, "Version", server.getVersion());
+ setAttribute(protobuf, "Official Distribution", officialDistribution.check());
+ setAttribute(protobuf, "Home Dir", config.get(PATH_HOME.getKey()).orElse(null));
+ setAttribute(protobuf, "Data Dir", config.get(PATH_DATA.getKey()).orElse(null));
+ setAttribute(protobuf, "Temp Dir", config.get(PATH_TEMP.getKey()).orElse(null));
+ setAttribute(protobuf, "Processors", Runtime.getRuntime().availableProcessors());
+ return protobuf.build();
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoader.java
new file mode 100644
index 00000000000..2c17b9666ab
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoader.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.platform.monitoring.cluster;
+
+
+import java.util.Collection;
+
+/**
+ * Loads "system information" of all Elasticsearch nodes.
+ */
+public interface SearchNodesInfoLoader {
+ Collection<NodeInfo> load();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImpl.java
new file mode 100644
index 00000000000..5ce39d0d9f2
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImpl.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.platform.monitoring.cluster;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
+import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
+import org.sonar.api.server.ServerSide;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsClient;
+import org.sonar.server.platform.monitoring.EsStateSection;
+
+@ServerSide
+public class SearchNodesInfoLoaderImpl implements SearchNodesInfoLoader {
+
+ private final EsClient esClient;
+
+ public SearchNodesInfoLoaderImpl(EsClient esClient) {
+ this.esClient = esClient;
+ }
+
+ public Collection<NodeInfo> load() {
+ NodesStatsResponse nodesStats = esClient.prepareNodesStats()
+ .setFs(true)
+ .setProcess(true)
+ .setJvm(true)
+ .setIndices(true)
+ .setBreaker(true)
+ .get();
+ List<NodeInfo> result = new ArrayList<>();
+ nodesStats.getNodes().forEach(nodeStat -> result.add(toNodeInfo(nodeStat)));
+ return result;
+ }
+
+ private static NodeInfo toNodeInfo(NodeStats stat) {
+ String nodeName = stat.getNode().getName();
+ NodeInfo info = new NodeInfo(nodeName);
+ info.setHost(stat.getHostname());
+
+ ProtobufSystemInfo.Section.Builder section = ProtobufSystemInfo.Section.newBuilder();
+ section.setName("Search State");
+ EsStateSection.toProtobuf(stat, section);
+ info.addSection(section.build());
+
+ return info;
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/package-info.java
new file mode 100644
index 00000000000..45cea3a7a14
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/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.monitoring.cluster;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/package-info.java
new file mode 100644
index 00000000000..895638cb3f2
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/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.monitoring;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/package-info.java
new file mode 100644
index 00000000000..71d187381a7
--- /dev/null
+++ b/server/sonar-webserver-core/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-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java
new file mode 100644
index 00000000000..e62ef12f2b5
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.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.serverid;
+
+import org.sonar.core.platform.ServerId;
+
+public interface ServerIdFactory {
+ /**
+ * Create a new ServerId from scratch.
+ */
+ ServerId create();
+
+ /**
+ * Create a new ServerId from the current serverId.
+ */
+ ServerId create(ServerId currentServerId);
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java
new file mode 100644
index 00000000000..8bee91eace9
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java
@@ -0,0 +1,69 @@
+/*
+ * 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.serverid;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.zip.CRC32;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.config.Configuration;
+import org.sonar.core.platform.ServerId;
+import org.sonar.core.util.UuidFactory;
+
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+
+public class ServerIdFactoryImpl implements ServerIdFactory {
+
+ private final Configuration config;
+ private final UuidFactory uuidFactory;
+ private final JdbcUrlSanitizer jdbcUrlSanitizer;
+
+ public ServerIdFactoryImpl(Configuration config, UuidFactory uuidFactory, JdbcUrlSanitizer jdbcUrlSanitizer) {
+ this.config = config;
+ this.uuidFactory = uuidFactory;
+ this.jdbcUrlSanitizer = jdbcUrlSanitizer;
+ }
+
+ @Override
+ public ServerId create() {
+ return ServerId.of(computeDatabaseId(), uuidFactory.create());
+ }
+
+ @Override
+ public ServerId create(ServerId currentServerId) {
+ return ServerId.of(computeDatabaseId(), currentServerId.getDatasetId());
+ }
+
+ private String computeDatabaseId() {
+ String jdbcUrl = config.get(JDBC_URL.getKey()).orElseThrow(() -> new IllegalStateException("Missing JDBC URL"));
+ return crc32Hex(jdbcUrlSanitizer.sanitize(jdbcUrl));
+ }
+
+ @VisibleForTesting
+ static String crc32Hex(String str) {
+ CRC32 crc32 = new CRC32();
+ crc32.update(str.getBytes(StandardCharsets.UTF_8));
+ long hash = crc32.getValue();
+ String s = Long.toHexString(hash).toUpperCase(Locale.ENGLISH);
+ return StringUtils.leftPad(s, ServerId.DATABASE_ID_LENGTH, "0");
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java
new file mode 100644
index 00000000000..5a4945150b0
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java
@@ -0,0 +1,163 @@
+/*
+ * 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.serverid;
+
+import java.util.Optional;
+import org.picocontainer.Startable;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.platform.ServerId;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.platform.WebServer;
+import org.sonar.server.property.InternalProperties;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.apache.commons.lang.StringUtils.isEmpty;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+import static org.sonar.api.CoreProperties.SERVER_ID;
+import static org.sonar.core.platform.ServerId.Format.DEPRECATED;
+import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_ID;
+import static org.sonar.server.property.InternalProperties.SERVER_ID_CHECKSUM;
+
+public class ServerIdManager implements Startable {
+ private static final Logger LOGGER = Loggers.get(ServerIdManager.class);
+
+ private final ServerIdChecksum serverIdChecksum;
+ private final ServerIdFactory serverIdFactory;
+ private final DbClient dbClient;
+ private final SonarRuntime runtime;
+ private final WebServer webServer;
+
+ public ServerIdManager(ServerIdChecksum serverIdChecksum, ServerIdFactory serverIdFactory, DbClient dbClient, SonarRuntime runtime, WebServer webServer) {
+ this.serverIdChecksum = serverIdChecksum;
+ this.serverIdFactory = serverIdFactory;
+ this.dbClient = dbClient;
+ this.runtime = runtime;
+ this.webServer = webServer;
+ }
+
+ @Override
+ public void start() {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ if (runtime.getSonarQubeSide() == SonarQubeSide.SERVER && webServer.isStartupLeader()) {
+ Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM);
+
+ ServerId serverId = readCurrentServerId(dbSession)
+ .map(currentServerId -> keepOrReplaceCurrentServerId(dbSession, currentServerId, checksum))
+ .orElseGet(() -> createFirstServerId(dbSession));
+ updateChecksum(dbSession, serverId);
+
+ dbSession.commit();
+ } else {
+ ensureServerIdIsValid(dbSession);
+ }
+ }
+ }
+
+ private ServerId keepOrReplaceCurrentServerId(DbSession dbSession, ServerId currentServerId, Optional<String> checksum) {
+ if (keepServerId(currentServerId, checksum)) {
+ return currentServerId;
+ }
+
+ ServerId serverId = replaceCurrentServerId(currentServerId);
+ persistServerId(dbSession, serverId);
+ return serverId;
+ }
+
+ private boolean keepServerId(ServerId serverId, Optional<String> checksum) {
+ ServerId.Format format = serverId.getFormat();
+ if (format == DEPRECATED || format == NO_DATABASE_ID) {
+ LOGGER.info("Server ID is changed to new format.");
+ return false;
+ }
+
+ if (checksum.isPresent()) {
+ String expectedChecksum = serverIdChecksum.computeFor(serverId.toString());
+ if (!expectedChecksum.equals(checksum.get())) {
+ LOGGER.warn("Server ID is reset because it is not valid anymore. Database URL probably changed. The new server ID affects SonarSource licensed products.");
+ return false;
+ }
+ }
+
+ // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does not exist.
+ return true;
+ }
+
+ private ServerId replaceCurrentServerId(ServerId currentServerId) {
+ if (currentServerId.getFormat() == DEPRECATED) {
+ return serverIdFactory.create();
+ }
+ return serverIdFactory.create(currentServerId);
+ }
+
+ private ServerId createFirstServerId(DbSession dbSession) {
+ ServerId serverId = serverIdFactory.create();
+ persistServerId(dbSession, serverId);
+ return serverId;
+ }
+
+ private Optional<ServerId> readCurrentServerId(DbSession dbSession) {
+ PropertyDto dto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID);
+ if (dto == null) {
+ return Optional.empty();
+ }
+
+ String value = dto.getValue();
+ if (isEmpty(value)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(ServerId.parse(value));
+ }
+
+ private void updateChecksum(DbSession dbSession, ServerId serverId) {
+ // checksum must be generated when it does not exist (upgrading to 6.7 or greater)
+ // or when server ID changed.
+ String checksum = serverIdChecksum.computeFor(serverId.toString());
+ persistChecksum(dbSession, checksum);
+ }
+
+ private void persistServerId(DbSession dbSession, ServerId serverId) {
+ dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(serverId.toString()));
+ }
+
+ private void persistChecksum(DbSession dbSession, String checksump) {
+ dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, checksump);
+ }
+
+ private void ensureServerIdIsValid(DbSession dbSession) {
+ PropertyDto id = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID);
+ checkState(id != null, "Property %s is missing in database", SERVER_ID);
+ checkState(isNotEmpty(id.getValue()), "Property %s is empty in database", SERVER_ID);
+
+ Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM);
+ checkState(checksum.isPresent(), "Internal property %s is missing in database", SERVER_ID_CHECKSUM);
+ checkState(checksum.get().equals(serverIdChecksum.computeFor(id.getValue())), "Server ID is invalid");
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java
new file mode 100644
index 00000000000..1f01a12704e
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.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.platform.serverid;
+
+import org.sonar.core.platform.Module;
+
+public class ServerIdModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ ServerIdFactoryImpl.class,
+ JdbcUrlSanitizer.class,
+ ServerIdChecksum.class,
+ ServerIdManager.class
+
+ );
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/package-info.java
new file mode 100644
index 00000000000..e7b4b5d7659
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/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.serverid;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/AbortTomcatStartException.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/AbortTomcatStartException.java
new file mode 100644
index 00000000000..66aa523acf2
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/AbortTomcatStartException.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.platform.web;
+
+/**
+ * An exception without any stacktrace nor message which point is solely to make tomcat startup fail during
+ * initialization of the Context Listener {@link PlatformServletContextListener}.
+ */
+public class AbortTomcatStartException extends RuntimeException {
+ public AbortTomcatStartException() {
+ super("Aborting tomcat servlet context startup");
+ }
+
+ /**
+ * Does not fill in the stack trace
+ *
+ * @see Throwable#fillInStackTrace()
+ */
+ @Override
+ public synchronized Throwable fillInStackTrace() {
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return getMessage();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/RootFilter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/RootFilter.java
new file mode 100644
index 00000000000..572444e5414
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/RootFilter.java
@@ -0,0 +1,116 @@
+/*
+ * 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.web;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+
+/**
+ * <p>Profile HTTP requests using platform profiling utility.</p>
+ * <p>To avoid profiling of requests for static resources, the <code>staticDirs</code>
+ * filter parameter can be set in the servlet context descriptor. This parameter should
+ * contain a comma-separated list of paths, starting at the context root;
+ * requests on subpaths of these paths will not be profiled.</p>
+ *
+ * @since 4.1
+ */
+public class RootFilter implements Filter {
+
+ private static final org.sonar.api.utils.log.Logger LOGGER = Loggers.get(RootFilter.class);
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ // nothing to do
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ if (request instanceof HttpServletRequest) {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ try {
+ chain.doFilter(new ServletRequestWrapper(httpRequest), httpResponse);
+ } catch (Throwable e) {
+ if (httpResponse.isCommitted()) {
+ // Request has been aborted by the client, nothing can been done as Tomcat has committed the response
+ LOGGER.debug(format("Processing of request %s failed", toUrl(httpRequest)), e);
+ return;
+ }
+ LOGGER.error(format("Processing of request %s failed", toUrl(httpRequest)), e);
+ httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ } else {
+ // Not an HTTP request, not profiled
+ chain.doFilter(request, response);
+ }
+ }
+
+ private static String toUrl(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ String queryString = request.getQueryString();
+ if (queryString == null) {
+ return requestURI;
+ }
+ return requestURI + '?' + queryString;
+ }
+
+ @Override
+ public void destroy() {
+ // Nothing
+ }
+
+ @VisibleForTesting
+ static class ServletRequestWrapper extends HttpServletRequestWrapper {
+
+ ServletRequestWrapper(HttpServletRequest request) {
+ super(request);
+ }
+
+ @Override
+ public HttpSession getSession(boolean create) {
+ if (!create) {
+ return null;
+ }
+ throw notSupported();
+ }
+
+ @Override
+ public HttpSession getSession() {
+ throw notSupported();
+ }
+
+ private static UnsupportedOperationException notSupported() {
+ return new UnsupportedOperationException("Sessions are disabled so that web server is stateless");
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/package-info.java
new file mode 100644
index 00000000000..0ca88f82702
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/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.web;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/HttpRequestIdModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/HttpRequestIdModule.java
new file mode 100644
index 00000000000..22fa06b2cc9
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/HttpRequestIdModule.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.web.requestid;
+
+import org.sonar.core.platform.Module;
+
+public class HttpRequestIdModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(new RequestIdConfiguration(RequestIdGeneratorImpl.UUID_GENERATOR_RENEWAL_COUNT),
+ RequestIdGeneratorBaseImpl.class,
+ RequestIdGeneratorImpl.class);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdConfiguration.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdConfiguration.java
new file mode 100644
index 00000000000..4acf5961341
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdConfiguration.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.platform.web.requestid;
+
+public class RequestIdConfiguration {
+ /**
+ * @see RequestIdGeneratorImpl#mustRenewUuidGenerator(long)
+ */
+ private final long uuidGeneratorRenewalCount;
+
+ public RequestIdConfiguration(long uuidGeneratorRenewalCount) {
+ this.uuidGeneratorRenewalCount = uuidGeneratorRenewalCount;
+ }
+
+ public long getUidGeneratorRenewalCount() {
+ return uuidGeneratorRenewalCount;
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGenerator.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGenerator.java
new file mode 100644
index 00000000000..4296285dd14
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGenerator.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.platform.web.requestid;
+
+/**
+ * Generate a Unique Identifier for Http Requests.
+ */
+public interface RequestIdGenerator {
+ /**
+ * Generate a new and unique request id for each call.
+ */
+ String generate();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBase.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBase.java
new file mode 100644
index 00000000000..c3781871a55
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBase.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.platform.web.requestid;
+
+import org.sonar.core.util.UuidGenerator;
+
+public interface RequestIdGeneratorBase {
+ /**
+ * Provides a new instance of {@link UuidGenerator.WithFixedBase} to be used by {@link RequestIdGeneratorImpl}.
+ */
+ UuidGenerator.WithFixedBase createNew();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBaseImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBaseImpl.java
new file mode 100644
index 00000000000..30979665ea1
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBaseImpl.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.web.requestid;
+
+import org.sonar.core.util.UuidGenerator;
+import org.sonar.core.util.UuidGeneratorImpl;
+
+public class RequestIdGeneratorBaseImpl implements RequestIdGeneratorBase {
+
+ @Override
+ public UuidGenerator.WithFixedBase createNew() {
+ return new UuidGeneratorImpl().withFixedBase();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImpl.java
new file mode 100644
index 00000000000..7010cac69de
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImpl.java
@@ -0,0 +1,96 @@
+/*
+ * 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.web.requestid;
+
+import java.util.Base64;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import org.sonar.core.util.UuidGenerator;
+
+/**
+ * This implementation of {@link RequestIdGenerator} creates unique identifiers for HTTP requests leveraging
+ * {@link UuidGenerator.WithFixedBase#generate(int)} and a counter of HTTP requests.
+ * <p>
+ * To work around the limit of unique values produced by {@link UuidGenerator.WithFixedBase#generate(int)}, the
+ * {@link UuidGenerator.WithFixedBase} instance will be renewed every
+ * {@link RequestIdConfiguration#getUidGeneratorRenewalCount() RequestIdConfiguration#uidGeneratorRenewalCount}
+ * HTTP requests.
+ * </p>
+ * <p>
+ * This implementation is Thread safe.
+ * </p>
+ */
+public class RequestIdGeneratorImpl implements RequestIdGenerator {
+ /**
+ * The value to which the HTTP request count will be compared to (using a modulo operator,
+ * see {@link #mustRenewUuidGenerator(long)}).
+ *
+ * <p>
+ * This value can't be the last value before {@link UuidGenerator.WithFixedBase#generate(int)} returns a non unique
+ * value, ie. 2^23-1 because there is no guarantee the renewal will happen before any other thread calls
+ * {@link UuidGenerator.WithFixedBase#generate(int)} method of the deplated {@link UuidGenerator.WithFixedBase} instance.
+ * </p>
+ *
+ * <p>
+ * To keep a comfortable margin of error, 2^22 will be used.
+ * </p>
+ */
+ public static final long UUID_GENERATOR_RENEWAL_COUNT = 4_194_304;
+
+ private final AtomicLong counter = new AtomicLong();
+ private final RequestIdGeneratorBase requestIdGeneratorBase;
+ private final RequestIdConfiguration requestIdConfiguration;
+ private final AtomicReference<UuidGenerator.WithFixedBase> uuidGenerator;
+
+ public RequestIdGeneratorImpl(RequestIdGeneratorBase requestIdGeneratorBase, RequestIdConfiguration requestIdConfiguration) {
+ this.requestIdGeneratorBase = requestIdGeneratorBase;
+ this.uuidGenerator = new AtomicReference<>(requestIdGeneratorBase.createNew());
+ this.requestIdConfiguration = requestIdConfiguration;
+ }
+
+ @Override
+ public String generate() {
+ UuidGenerator.WithFixedBase currentUuidGenerator = this.uuidGenerator.get();
+ long counterValue = counter.getAndIncrement();
+ if (counterValue != 0 && mustRenewUuidGenerator(counterValue)) {
+ UuidGenerator.WithFixedBase newUuidGenerator = requestIdGeneratorBase.createNew();
+ uuidGenerator.set(newUuidGenerator);
+ return generate(newUuidGenerator, counterValue);
+ }
+ return generate(currentUuidGenerator, counterValue);
+ }
+
+ /**
+ * Since renewal of {@link UuidGenerator.WithFixedBase} instance is based on the HTTP request counter, only a single
+ * thread can get the right value which will make this method return true. So, this is thread-safe by design, therefor
+ * this method doesn't need external synchronization.
+ * <p>
+ * The value to which the counter is compared should however be chosen with caution: see {@link #UUID_GENERATOR_RENEWAL_COUNT}.
+ * </p>
+ */
+ private boolean mustRenewUuidGenerator(long counter) {
+ return counter % requestIdConfiguration.getUidGeneratorRenewalCount() == 0;
+ }
+
+ private static String generate(UuidGenerator.WithFixedBase uuidGenerator, long increment) {
+ return Base64.getEncoder().encodeToString(uuidGenerator.generate((int) increment));
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdMDCStorage.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdMDCStorage.java
new file mode 100644
index 00000000000..53b4bf49e33
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdMDCStorage.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.platform.web.requestid;
+
+import org.slf4j.MDC;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Wraps MDC calls to store the HTTP request ID in the {@link MDC} into an {@link AutoCloseable}.
+ */
+public class RequestIdMDCStorage implements AutoCloseable {
+ public static final String HTTP_REQUEST_ID_MDC_KEY = "HTTP_REQUEST_ID";
+
+ public RequestIdMDCStorage(String requestId) {
+ MDC.put(HTTP_REQUEST_ID_MDC_KEY, requireNonNull(requestId, "Request ID can't be null"));
+ }
+
+ @Override
+ public void close() {
+ MDC.remove(HTTP_REQUEST_ID_MDC_KEY);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/package-info.java
new file mode 100644
index 00000000000..413da7d0492
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/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.web.requestid;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CachingRuleFinder.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CachingRuleFinder.java
new file mode 100644
index 00000000000..bd6995ab93b
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CachingRuleFinder.java
@@ -0,0 +1,197 @@
+/*
+ * 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.rule;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Ordering;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.api.rules.RuleQuery;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.markdown.Markdown;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+/**
+ * A {@link RuleFinder} implementation that retrieves all rule definitions and their parameter when instantiated, cache
+ * them in memory and provide implementation of {@link RuleFinder}'s method which only read from this data in memory.
+ */
+public class CachingRuleFinder implements RuleFinder {
+
+ private static final Ordering<Map.Entry<RuleDefinitionDto, Rule>> FIND_BY_QUERY_ORDER = Ordering.natural().reverse().onResultOf(entry -> entry.getKey().getUpdatedAt());
+
+ private final Map<RuleDefinitionDto, Rule> rulesByRuleDefinition;
+ private final Map<Integer, Rule> rulesById;
+ private final Map<RuleKey, Rule> rulesByKey;
+
+ public CachingRuleFinder(DbClient dbClient) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ this.rulesByRuleDefinition = buildRulesByRuleDefinitionDto(dbClient, dbSession);
+ this.rulesById = this.rulesByRuleDefinition.entrySet().stream()
+ .collect(uniqueIndex(entry -> entry.getKey().getId(), Map.Entry::getValue));
+ this.rulesByKey = this.rulesByRuleDefinition.entrySet().stream()
+ .collect(uniqueIndex(entry -> entry.getKey().getKey(), Map.Entry::getValue));
+ }
+ }
+
+ private static Map<RuleDefinitionDto, Rule> buildRulesByRuleDefinitionDto(DbClient dbClient, DbSession dbSession) {
+ List<RuleDefinitionDto> dtos = dbClient.ruleDao().selectAllDefinitions(dbSession);
+ Set<RuleKey> ruleKeys = dtos.stream().map(RuleDefinitionDto::getKey).collect(toSet(dtos.size()));
+ ListMultimap<Integer, RuleParamDto> ruleParamsByRuleId = retrieveRuleParameters(dbClient, dbSession, ruleKeys);
+ Map<RuleDefinitionDto, Rule> rulesByDefinition = new HashMap<>(dtos.size());
+ for (RuleDefinitionDto definition : dtos) {
+ rulesByDefinition.put(definition, toRule(definition, ruleParamsByRuleId.get(definition.getId())));
+ }
+ return ImmutableMap.copyOf(rulesByDefinition);
+ }
+
+ private static ImmutableListMultimap<Integer, RuleParamDto> retrieveRuleParameters(DbClient dbClient, DbSession dbSession, Set<RuleKey> ruleKeys) {
+ if (ruleKeys.isEmpty()) {
+ return ImmutableListMultimap.of();
+ }
+ return dbClient.ruleDao().selectRuleParamsByRuleKeys(dbSession, ruleKeys)
+ .stream()
+ .collect(MoreCollectors.index(RuleParamDto::getRuleId));
+ }
+
+ @Override
+ @Deprecated
+ @CheckForNull
+ public Rule findById(int ruleId) {
+ return rulesById.get(ruleId);
+ }
+
+ @Override
+ @CheckForNull
+ public Rule findByKey(@Nullable String repositoryKey, @Nullable String key) {
+ if (repositoryKey == null || key == null) {
+ return null;
+ }
+ return findByKey(RuleKey.of(repositoryKey, key));
+ }
+
+ @Override
+ @CheckForNull
+ public Rule findByKey(RuleKey key) {
+ return rulesByKey.get(key);
+ }
+
+ @Override
+ @CheckForNull
+ public Rule find(@Nullable RuleQuery query) {
+ if (query == null) {
+ return null;
+ }
+
+ return rulesByRuleDefinition.entrySet().stream()
+ .filter(entry -> matchQuery(entry.getKey(), query))
+ .sorted(FIND_BY_QUERY_ORDER)
+ .map(Map.Entry::getValue)
+ .findFirst()
+ .orElse(null);
+ }
+
+ @Override
+ public Collection<Rule> findAll(@Nullable RuleQuery query) {
+ if (query == null) {
+ return Collections.emptyList();
+ }
+ return rulesByRuleDefinition.entrySet().stream()
+ .filter(entry -> matchQuery(entry.getKey(), query))
+ .sorted(FIND_BY_QUERY_ORDER)
+ .map(Map.Entry::getValue)
+ .collect(MoreCollectors.toList());
+ }
+
+ private static boolean matchQuery(RuleDefinitionDto ruleDefinition, RuleQuery ruleQuery) {
+ if (RuleStatus.REMOVED.equals(ruleDefinition.getStatus())) {
+ return false;
+ }
+ String repositoryKey = ruleQuery.getRepositoryKey();
+ if (ruleQuery.getRepositoryKey() != null && !repositoryKey.equals(ruleDefinition.getRepositoryKey())) {
+ return false;
+ }
+ String key = ruleQuery.getKey();
+ if (key != null && !key.equals(ruleDefinition.getRuleKey())) {
+ return false;
+ }
+ String configKey = ruleQuery.getConfigKey();
+ return configKey == null || configKey.equals(ruleDefinition.getConfigKey());
+ }
+
+ private static Rule toRule(RuleDefinitionDto ruleDefinition, List<RuleParamDto> params) {
+ String severity = ruleDefinition.getSeverityString();
+ String description = ruleDefinition.getDescription();
+ RuleDto.Format descriptionFormat = ruleDefinition.getDescriptionFormat();
+
+ Rule apiRule = new Rule();
+ apiRule
+ .setName(ruleDefinition.getName())
+ .setLanguage(ruleDefinition.getLanguage())
+ .setKey(ruleDefinition.getRuleKey())
+ .setConfigKey(ruleDefinition.getConfigKey())
+ .setIsTemplate(ruleDefinition.isTemplate())
+ .setCreatedAt(new Date(ruleDefinition.getCreatedAt()))
+ .setUpdatedAt(new Date(ruleDefinition.getUpdatedAt()))
+ .setRepositoryKey(ruleDefinition.getRepositoryKey())
+ .setSeverity(severity != null ? RulePriority.valueOf(severity) : null)
+ .setStatus(ruleDefinition.getStatus().name())
+ .setSystemTags(ruleDefinition.getSystemTags().toArray(new String[ruleDefinition.getSystemTags().size()]))
+ .setTags(new String[0])
+ .setId(ruleDefinition.getId());
+ if (description != null && descriptionFormat != null) {
+ if (RuleDto.Format.HTML.equals(descriptionFormat)) {
+ apiRule.setDescription(description);
+ } else {
+ apiRule.setDescription(Markdown.convertToHtml(description));
+ }
+ }
+
+ List<org.sonar.api.rules.RuleParam> apiParams = newArrayList();
+ for (RuleParamDto param : params) {
+ apiParams.add(new org.sonar.api.rules.RuleParam(apiRule, param.getName(), param.getDescription(), param.getType())
+ .setDefaultValue(param.getDefaultValue()));
+ }
+ apiRule.setParams(apiParams);
+
+ return apiRule;
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitions.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitions.java
new file mode 100644
index 00000000000..252b0c1e332
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitions.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.rule;
+
+import org.sonar.api.server.rule.RulesDefinition;
+
+public interface CommonRuleDefinitions {
+ void define(RulesDefinition.Context context);
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitionsImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitionsImpl.java
new file mode 100644
index 00000000000..0a9c5407950
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitionsImpl.java
@@ -0,0 +1,135 @@
+/*
+ * 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.rule;
+
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static org.sonar.server.rule.CommonRuleKeys.commonRepositoryForLang;
+
+/**
+ * Declare the few rules that are automatically created by core for all languages. These rules
+ * check measure values against thresholds defined in Quality profiles.
+ */
+// this class must not be mixed with other RulesDefinition so it does implement the interface RulesDefinitions.
+// It replaces the common-rules that are still embedded within plugins.
+public class CommonRuleDefinitionsImpl implements CommonRuleDefinitions {
+
+ private final Languages languages;
+
+ public CommonRuleDefinitionsImpl(Languages languages) {
+ this.languages = languages;
+ }
+
+ @Override
+ public void define(RulesDefinition.Context context) {
+ for (Language language : languages.all()) {
+ RulesDefinition.NewRepository repo = context.createRepository(commonRepositoryForLang(language.getKey()), language.getKey());
+ repo.setName("Common " + language.getName());
+ defineBranchCoverageRule(repo);
+ defineLineCoverageRule(repo);
+ defineCommentDensityRule(repo);
+ defineDuplicatedBlocksRule(repo);
+ defineFailedUnitTestRule(repo);
+ defineSkippedUnitTestRule(repo);
+ repo.done();
+ }
+ }
+
+ private static void defineBranchCoverageRule(RulesDefinition.NewRepository repo) {
+ RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.INSUFFICIENT_BRANCH_COVERAGE);
+ rule.setName("Branches should have sufficient coverage by tests")
+ .addTags("bad-practice")
+ .setHtmlDescription("An issue is created on a file as soon as the branch coverage on this file is less than the required threshold. "
+ + "It gives the number of branches to be covered in order to reach the required threshold.")
+ .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("5min"))
+ .setGapDescription("number of uncovered conditions")
+ .setSeverity(Severity.MAJOR);
+ rule.createParam(CommonRuleKeys.INSUFFICIENT_BRANCH_COVERAGE_PROPERTY)
+ .setName("The minimum required branch coverage ratio")
+ .setDefaultValue("65")
+ .setType(RuleParamType.FLOAT);
+ }
+
+ private static void defineLineCoverageRule(RulesDefinition.NewRepository repo) {
+ RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.INSUFFICIENT_LINE_COVERAGE);
+ rule.setName("Lines should have sufficient coverage by tests")
+ .addTags("bad-practice")
+ .setHtmlDescription("An issue is created on a file as soon as the line coverage on this file is less than the required threshold. " +
+ "It gives the number of lines to be covered in order to reach the required threshold.")
+ .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("2min"))
+ .setGapDescription("number of lines under the coverage threshold")
+ .setSeverity(Severity.MAJOR);
+ rule.createParam(CommonRuleKeys.INSUFFICIENT_LINE_COVERAGE_PROPERTY)
+ .setName("The minimum required line coverage ratio")
+ .setDefaultValue("65")
+ .setType(RuleParamType.FLOAT);
+ }
+
+ private static void defineCommentDensityRule(RulesDefinition.NewRepository repo) {
+ RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.INSUFFICIENT_COMMENT_DENSITY);
+ rule.setName("Source files should have a sufficient density of comment lines")
+ .addTags("convention")
+ .setHtmlDescription("An issue is created on a file as soon as the density of comment lines on this file is less than the required threshold. " +
+ "The number of comment lines to be written in order to reach the required threshold is provided by each issue message.")
+ .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("2min"))
+ .setGapDescription("number of lines required to meet minimum density")
+ .setSeverity(Severity.MAJOR);
+ rule.createParam(CommonRuleKeys.INSUFFICIENT_COMMENT_DENSITY_PROPERTY)
+ .setName("The minimum required comment density")
+ .setDefaultValue("25")
+ .setType(RuleParamType.FLOAT);
+ }
+
+ private static void defineDuplicatedBlocksRule(RulesDefinition.NewRepository repo) {
+ RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.DUPLICATED_BLOCKS);
+ rule.setName("Source files should not have any duplicated blocks")
+ .addTags("pitfall")
+ .setHtmlDescription("An issue is created on a file as soon as there is at least one block of duplicated code on this file")
+ .setDebtRemediationFunction(rule.debtRemediationFunctions().linearWithOffset("10min", "10min"))
+ .setGapDescription("number of duplicate blocks")
+ .setSeverity(Severity.MAJOR);
+ }
+
+ private static void defineFailedUnitTestRule(RulesDefinition.NewRepository repo) {
+ RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.FAILED_UNIT_TESTS);
+ rule
+ .setName("Failed unit tests should be fixed")
+ .addTags("bug")
+ .setHtmlDescription(
+ "Test failures or errors generally indicate that regressions have been introduced. Those tests should be handled as soon as possible to reduce the cost to fix the corresponding regressions.")
+ .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("10min"))
+ .setGapDescription("number of failed tests")
+ .setSeverity(Severity.MAJOR);
+ }
+
+ private static void defineSkippedUnitTestRule(RulesDefinition.NewRepository repo) {
+ RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.SKIPPED_UNIT_TESTS);
+ rule.setName("Skipped unit tests should be either removed or fixed")
+ .addTags("pitfall")
+ .setHtmlDescription("Skipped unit tests are considered as dead code. Either they should be activated again (and updated) or they should be removed.")
+ .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("10min"))
+ .setGapDescription("number of skipped tests")
+ .setSeverity(Severity.MAJOR);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java
new file mode 100644
index 00000000000..a6e255ed80f
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java
@@ -0,0 +1,216 @@
+/*
+ * 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.rule;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import java.io.Reader;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.RuleParam;
+import org.sonar.api.rules.RuleRepository;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.ValidationMessages;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.i18n.RuleI18nManager;
+import org.sonar.server.debt.DebtModelPluginRepository;
+import org.sonar.server.debt.DebtModelXMLExporter;
+import org.sonar.server.debt.DebtModelXMLExporter.RuleDebt;
+import org.sonar.server.debt.DebtRulesXMLImporter;
+import org.sonar.server.plugins.ServerPluginRepository;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * Inject deprecated RuleRepository into {@link org.sonar.api.server.rule.RulesDefinition} for backward-compatibility.
+ */
+@ServerSide
+public class DeprecatedRulesDefinitionLoader {
+
+ private static final Logger LOG = Loggers.get(DeprecatedRulesDefinitionLoader.class);
+
+ private final RuleI18nManager i18n;
+ private final RuleRepository[] repositories;
+
+ private final DebtModelPluginRepository languageModelFinder;
+ private final DebtRulesXMLImporter importer;
+ private final ServerPluginRepository serverPluginRepository;
+
+ public DeprecatedRulesDefinitionLoader(RuleI18nManager i18n, DebtModelPluginRepository languageModelFinder, DebtRulesXMLImporter importer,
+ ServerPluginRepository serverPluginRepository, RuleRepository[] repositories) {
+ this.i18n = i18n;
+ this.serverPluginRepository = serverPluginRepository;
+ this.repositories = repositories;
+ this.languageModelFinder = languageModelFinder;
+ this.importer = importer;
+ }
+
+ /**
+ * Used when no deprecated repositories
+ */
+ public DeprecatedRulesDefinitionLoader(RuleI18nManager i18n, DebtModelPluginRepository languageModelFinder, DebtRulesXMLImporter importer,
+ ServerPluginRepository serverPluginRepository) {
+ this(i18n, languageModelFinder, importer, serverPluginRepository, new RuleRepository[0]);
+ }
+
+ void complete(RulesDefinition.Context context) {
+ // Load rule debt definitions from xml files provided by plugin
+ List<RuleDebt> ruleDebts = loadRuleDebtList();
+
+ for (RuleRepository repository : repositories) {
+ context.setCurrentPluginKey(serverPluginRepository.getPluginKey(repository));
+ // RuleRepository API does not handle difference between new and extended repositories,
+ RulesDefinition.NewRepository newRepository;
+ if (context.repository(repository.getKey()) == null) {
+ newRepository = context.createRepository(repository.getKey(), repository.getLanguage());
+ newRepository.setName(repository.getName());
+ } else {
+ newRepository = context.extendRepository(repository.getKey(), repository.getLanguage());
+ }
+ for (org.sonar.api.rules.Rule rule : repository.createRules()) {
+ RulesDefinition.NewRule newRule = newRepository.createRule(rule.getKey());
+ newRule.setName(ruleName(repository.getKey(), rule));
+ newRule.setHtmlDescription(ruleDescription(repository.getKey(), rule));
+ newRule.setInternalKey(rule.getConfigKey());
+ newRule.setTemplate(rule.isTemplate());
+ newRule.setSeverity(rule.getSeverity().toString());
+ newRule.setStatus(rule.getStatus() == null ? RuleStatus.defaultStatus() : RuleStatus.valueOf(rule.getStatus()));
+ newRule.setTags(rule.getTags());
+ for (RuleParam param : rule.getParams()) {
+ RulesDefinition.NewParam newParam = newRule.createParam(param.getKey());
+ newParam.setDefaultValue(param.getDefaultValue());
+ newParam.setDescription(paramDescription(repository.getKey(), rule.getKey(), param));
+ newParam.setType(RuleParamType.parse(param.getType()));
+ }
+ updateRuleDebtDefinitions(newRule, repository.getKey(), rule.getKey(), ruleDebts);
+ }
+ newRepository.done();
+ }
+ }
+
+ private static void updateRuleDebtDefinitions(RulesDefinition.NewRule newRule, String repoKey, String ruleKey, List<RuleDebt> ruleDebts) {
+ RuleDebt ruleDebt = findRequirement(ruleDebts, repoKey, ruleKey);
+ if (ruleDebt != null) {
+ newRule.setDebtRemediationFunction(remediationFunction(DebtRemediationFunction.Type.valueOf(ruleDebt.function()),
+ ruleDebt.coefficient(),
+ ruleDebt.offset(),
+ newRule.debtRemediationFunctions(),
+ repoKey, ruleKey));
+ }
+ }
+
+ private static DebtRemediationFunction remediationFunction(DebtRemediationFunction.Type function, @Nullable String coefficient, @Nullable String offset,
+ RulesDefinition.DebtRemediationFunctions functions, String repoKey, String ruleKey) {
+ if (DebtRemediationFunction.Type.LINEAR.equals(function) && coefficient != null) {
+ return functions.linear(coefficient);
+ } else if (DebtRemediationFunction.Type.CONSTANT_ISSUE.equals(function) && offset != null) {
+ return functions.constantPerIssue(offset);
+ } else if (DebtRemediationFunction.Type.LINEAR_OFFSET.equals(function) && coefficient != null && offset != null) {
+ return functions.linearWithOffset(coefficient, offset);
+ } else {
+ throw new IllegalArgumentException(String.format("Debt definition on rule '%s:%s' is invalid", repoKey, ruleKey));
+ }
+ }
+
+ @CheckForNull
+ private String ruleName(String repositoryKey, org.sonar.api.rules.Rule rule) {
+ String name = i18n.getName(repositoryKey, rule.getKey());
+ if (StringUtils.isNotBlank(name)) {
+ return name;
+ }
+ return StringUtils.defaultIfBlank(rule.getName(), null);
+ }
+
+ @CheckForNull
+ private String ruleDescription(String repositoryKey, org.sonar.api.rules.Rule rule) {
+ String description = i18n.getDescription(repositoryKey, rule.getKey());
+ if (StringUtils.isNotBlank(description)) {
+ return description;
+ }
+ return StringUtils.defaultIfBlank(rule.getDescription(), null);
+ }
+
+ @CheckForNull
+ private String paramDescription(String repositoryKey, String ruleKey, RuleParam param) {
+ String desc = StringUtils.defaultIfEmpty(
+ i18n.getParamDescription(repositoryKey, ruleKey, param.getKey()),
+ param.getDescription());
+ return StringUtils.defaultIfBlank(desc, null);
+ }
+
+ public List<DebtModelXMLExporter.RuleDebt> loadRuleDebtList() {
+ List<RuleDebt> ruleDebtList = newArrayList();
+ for (String pluginKey : getContributingPluginListWithoutSqale()) {
+ ruleDebtList.addAll(loadRuleDebtsFromXml(pluginKey));
+ }
+ return ruleDebtList;
+ }
+
+ public List<RuleDebt> loadRuleDebtsFromXml(String pluginKey) {
+ Reader xmlFileReader = null;
+ try {
+ xmlFileReader = languageModelFinder.createReaderForXMLFile(pluginKey);
+ ValidationMessages validationMessages = ValidationMessages.create();
+ List<RuleDebt> rules = importer.importXML(xmlFileReader, validationMessages);
+ validationMessages.log(LOG);
+ return rules;
+ } finally {
+ IOUtils.closeQuietly(xmlFileReader);
+ }
+ }
+
+ private Collection<String> getContributingPluginListWithoutSqale() {
+ Collection<String> pluginList = newArrayList(languageModelFinder.getContributingPluginList());
+ pluginList.remove(DebtModelPluginRepository.DEFAULT_MODEL);
+ return pluginList;
+ }
+
+ @CheckForNull
+ private static RuleDebt findRequirement(List<RuleDebt> requirements, final String repoKey, final String ruleKey) {
+ return Iterables.find(requirements, new RuleDebtMatchRepoKeyAndRuleKey(repoKey, ruleKey), null);
+ }
+
+ private static class RuleDebtMatchRepoKeyAndRuleKey implements Predicate<RuleDebt> {
+
+ private final String repoKey;
+ private final String ruleKey;
+
+ public RuleDebtMatchRepoKeyAndRuleKey(String repoKey, String ruleKey) {
+ this.repoKey = repoKey;
+ this.ruleKey = ruleKey;
+ }
+
+ @Override
+ public boolean apply(@Nonnull RuleDebt input) {
+ return input.ruleKey().equals(RuleKey.of(repoKey, ruleKey));
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/Rule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/Rule.java
new file mode 100644
index 00000000000..5d6a2a32339
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/Rule.java
@@ -0,0 +1,110 @@
+/*
+ * 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.rule;
+
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+
+import javax.annotation.CheckForNull;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @since 4.4
+ */
+public interface Rule {
+
+ RuleKey key();
+
+ String language();
+
+ String name();
+
+ @CheckForNull
+ String htmlDescription();
+
+ @CheckForNull
+ String markdownDescription();
+
+ String effortToFixDescription();
+
+ /**
+ * Default severity when activated on a Quality profile
+ *
+ * @see org.sonar.api.rule.Severity
+ */
+ String severity();
+
+ /**
+ * @see org.sonar.api.rule.RuleStatus
+ */
+ RuleStatus status();
+
+ boolean isTemplate();
+
+ @CheckForNull
+ RuleKey templateKey();
+
+ /**
+ * Tags that can be customized by administrators
+ */
+ List<String> tags();
+
+ /**
+ * Read-only tags defined by plugins
+ */
+ List<String> systemTags();
+
+ List<RuleParam> params();
+
+ @CheckForNull
+ RuleParam param(final String key);
+
+ boolean debtOverloaded();
+
+ @CheckForNull
+ DebtRemediationFunction debtRemediationFunction();
+
+ @CheckForNull
+ DebtRemediationFunction defaultDebtRemediationFunction();
+
+ Date createdAt();
+
+ Date updatedAt();
+
+ @CheckForNull
+ String internalKey();
+
+ @CheckForNull
+ String markdownNote();
+
+ @CheckForNull
+ String noteLogin();
+
+ @CheckForNull
+ Date noteCreatedAt();
+
+ @CheckForNull
+ Date noteUpdatedAt();
+
+ boolean isManual();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDefinitionsLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDefinitionsLoader.java
new file mode 100644
index 00000000000..80da97d352f
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDefinitionsLoader.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.rule;
+
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.impl.server.RulesDefinitionContext;
+import org.sonar.server.plugins.ServerPluginRepository;
+
+/**
+ * Loads all instances of {@link RulesDefinition}. Used during server startup
+ * and restore of debt model backup.
+ */
+public class RuleDefinitionsLoader {
+
+ private final DeprecatedRulesDefinitionLoader deprecatedDefConverter;
+ private final CommonRuleDefinitions coreCommonDefs;
+ private final RulesDefinition[] pluginDefs;
+ private final ServerPluginRepository serverPluginRepository;
+
+ public RuleDefinitionsLoader(DeprecatedRulesDefinitionLoader deprecatedDefConverter,
+ CommonRuleDefinitions coreCommonDefs, ServerPluginRepository serverPluginRepository, RulesDefinition[] pluginDefs) {
+ this.deprecatedDefConverter = deprecatedDefConverter;
+ this.coreCommonDefs = coreCommonDefs;
+ this.serverPluginRepository = serverPluginRepository;
+ this.pluginDefs = pluginDefs;
+ }
+
+ /**
+ * Used when no definitions at all.
+ */
+ public RuleDefinitionsLoader(DeprecatedRulesDefinitionLoader converter,
+ CommonRuleDefinitions coreCommonDefs, ServerPluginRepository serverPluginRepository) {
+ this(converter, coreCommonDefs, serverPluginRepository, new RulesDefinition[0]);
+ }
+
+ public RulesDefinition.Context load() {
+ RulesDefinition.Context context = new RulesDefinitionContext();
+ for (RulesDefinition pluginDefinition : pluginDefs) {
+ context.setCurrentPluginKey(serverPluginRepository.getPluginKey(pluginDefinition));
+ pluginDefinition.define(context);
+ }
+ deprecatedDefConverter.complete(context);
+ context.setCurrentPluginKey(null);
+ coreCommonDefs.define(context);
+ return context;
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleParam.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleParam.java
new file mode 100644
index 00000000000..0a65abd961b
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleParam.java
@@ -0,0 +1,37 @@
+/*
+ * 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.rule;
+
+import org.sonar.api.server.rule.RuleParamType;
+
+import javax.annotation.CheckForNull;
+
+public interface RuleParam {
+
+ String key();
+
+ @CheckForNull
+ String description();
+
+ @CheckForNull
+ String defaultValue();
+
+ RuleParamType type();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java
new file mode 100644
index 00000000000..b96d906bf71
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java
@@ -0,0 +1,154 @@
+/*
+ * 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.rule;
+
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+
+@Immutable
+class SingleDeprecatedRuleKey {
+ private String oldRuleKey;
+ private String oldRepositoryKey;
+ private String newRuleKey;
+ private String newRepositoryKey;
+ private String uuid;
+ private Integer ruleId;
+
+ /**
+ * static methods {@link #from(RulesDefinition.Rule)} and {@link #from(DeprecatedRuleKeyDto)} must be used
+ */
+ private SingleDeprecatedRuleKey() {
+ // empty
+ }
+
+ public static Set<SingleDeprecatedRuleKey> from(RulesDefinition.Rule rule) {
+ return rule.deprecatedRuleKeys().stream()
+ .map(r -> new SingleDeprecatedRuleKey()
+ .setNewRepositoryKey(rule.repository().key())
+ .setNewRuleKey(rule.key())
+ .setOldRepositoryKey(r.repository())
+ .setOldRuleKey(r.rule()))
+ .collect(MoreCollectors.toSet(rule.deprecatedRuleKeys().size()));
+ }
+
+ public static SingleDeprecatedRuleKey from(DeprecatedRuleKeyDto rule) {
+ return new SingleDeprecatedRuleKey()
+ .setUuid(rule.getUuid())
+ .setRuleId(rule.getRuleId())
+ .setNewRepositoryKey(rule.getNewRepositoryKey())
+ .setNewRuleKey(rule.getNewRuleKey())
+ .setOldRepositoryKey(rule.getOldRepositoryKey())
+ .setOldRuleKey(rule.getOldRuleKey());
+ }
+
+ public String getOldRuleKey() {
+ return oldRuleKey;
+ }
+
+ public String getOldRepositoryKey() {
+ return oldRepositoryKey;
+ }
+
+ public RuleKey getOldRuleKeyAsRuleKey() {
+ return RuleKey.of(oldRepositoryKey, oldRuleKey);
+ }
+
+
+ public RuleKey getNewRuleKeyAsRuleKey() {
+ return RuleKey.of(newRepositoryKey, newRuleKey);
+ }
+
+ @CheckForNull
+ public String getNewRuleKey() {
+ return newRuleKey;
+ }
+
+ @CheckForNull
+ public String getNewRepositoryKey() {
+ return newRepositoryKey;
+ }
+
+ @CheckForNull
+ public String getUuid() {
+ return uuid;
+ }
+
+ @CheckForNull
+ public Integer getRuleId() {
+ return ruleId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SingleDeprecatedRuleKey)) {
+ return false;
+ }
+ SingleDeprecatedRuleKey that = (SingleDeprecatedRuleKey) o;
+ return Objects.equals(oldRuleKey, that.oldRuleKey) &&
+ Objects.equals(oldRepositoryKey, that.oldRepositoryKey) &&
+ Objects.equals(newRuleKey, that.newRuleKey) &&
+ Objects.equals(newRepositoryKey, that.newRepositoryKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(oldRuleKey, oldRepositoryKey, newRuleKey, newRepositoryKey);
+ }
+
+ private SingleDeprecatedRuleKey setRuleId(Integer ruleId) {
+ this.ruleId = ruleId;
+ return this;
+ }
+
+ private SingleDeprecatedRuleKey setUuid(String uuid) {
+ this.uuid = uuid;
+ return this;
+ }
+
+ private SingleDeprecatedRuleKey setOldRuleKey(String oldRuleKey) {
+ this.oldRuleKey = oldRuleKey;
+ return this;
+ }
+
+ private SingleDeprecatedRuleKey setOldRepositoryKey(String oldRepositoryKey) {
+ this.oldRepositoryKey = oldRepositoryKey;
+ return this;
+ }
+
+ private SingleDeprecatedRuleKey setNewRuleKey(@Nullable String newRuleKey) {
+ this.newRuleKey = newRuleKey;
+ return this;
+ }
+
+ private SingleDeprecatedRuleKey setNewRepositoryKey(@Nullable String newRepositoryKey) {
+ this.newRepositoryKey = newRepositoryKey;
+ return this;
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinder.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinder.java
new file mode 100644
index 00000000000..aadbb34d40b
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinder.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.rule;
+
+import org.sonar.api.rules.RuleFinder;
+
+/**
+ * {@link RuleFinder} implementation that supports caching used by the Web Server.
+ * <p>
+ * Caching is enabled right after loading of rules is done (see {@link RegisterRules}) and disabled
+ * once all startup tasks are done (see {@link org.sonar.server.platform.platformlevel.PlatformLevelStartup}).
+ * </p>
+ */
+public interface WebServerRuleFinder extends RuleFinder {
+ /**
+ * Enable caching.
+ */
+ void startCaching();
+
+ /**
+ * Disable caching.
+ */
+ void stopCaching();
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinderImpl.java
new file mode 100644
index 00000000000..db347d46132
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinderImpl.java
@@ -0,0 +1,84 @@
+/*
+ * 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.rule;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Collection;
+import javax.annotation.CheckForNull;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.rules.RuleQuery;
+import org.sonar.db.DbClient;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+
+public class WebServerRuleFinderImpl implements WebServerRuleFinder {
+ private final DbClient dbClient;
+ private final RuleFinder defaultFinder;
+ @VisibleForTesting
+ RuleFinder delegate;
+
+ public WebServerRuleFinderImpl(DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider) {
+ this.dbClient = dbClient;
+ this.defaultFinder = new DefaultRuleFinder(dbClient, defaultOrganizationProvider);
+ this.delegate = this.defaultFinder;
+ }
+
+ @Override
+ public void startCaching() {
+ this.delegate = new CachingRuleFinder(dbClient);
+ }
+
+ @Override
+ public void stopCaching() {
+ this.delegate = this.defaultFinder;
+ }
+
+ @Override
+ @CheckForNull
+ @Deprecated
+ public Rule findById(int ruleId) {
+ return delegate.findById(ruleId);
+ }
+
+ @Override
+ @CheckForNull
+ public Rule findByKey(String repositoryKey, String key) {
+ return delegate.findByKey(repositoryKey, key);
+ }
+
+ @Override
+ @CheckForNull
+ public Rule findByKey(RuleKey key) {
+ return delegate.findByKey(key);
+ }
+
+ @Override
+ @CheckForNull
+ public Rule find(RuleQuery query) {
+ return delegate.find(query);
+ }
+
+ @Override
+ public Collection<Rule> findAll(RuleQuery query) {
+ return delegate.findAll(query);
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/package-info.java
new file mode 100644
index 00000000000..09d28ba5169
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.rule;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java
new file mode 100644
index 00000000000..22e5bf4257e
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java
@@ -0,0 +1,97 @@
+/*
+ * 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.startup;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.CharUtils;
+import org.sonar.api.Startable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.server.plugins.InstalledPlugin;
+import org.sonar.server.plugins.PluginFileSystem;
+
+/**
+ * The file deploy/plugins/index.txt is required for old versions of SonarLint.
+ * They don't use the web service api/plugins/installed to get the list
+ * of installed plugins.
+ * https://jira.sonarsource.com/browse/SLCORE-146
+ */
+@ServerSide
+public final class GeneratePluginIndex implements Startable {
+
+ private static final Logger LOG = Loggers.get(GeneratePluginIndex.class);
+
+ private final ServerFileSystem serverFs;
+ private final PluginFileSystem pluginFs;
+
+ public GeneratePluginIndex(ServerFileSystem serverFs, PluginFileSystem pluginFs) {
+ this.serverFs = serverFs;
+ this.pluginFs = pluginFs;
+ }
+
+ @Override
+ public void start() {
+ Profiler profiler = Profiler.create(LOG).startInfo("Generate scanner plugin index");
+ writeIndex(serverFs.getPluginIndex());
+ profiler.stopDebug();
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+
+ private void writeIndex(File indexFile) {
+ try {
+ FileUtils.forceMkdir(indexFile.getParentFile());
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(indexFile), StandardCharsets.UTF_8)) {
+ for (InstalledPlugin plugin : pluginFs.getInstalledFiles()) {
+ writer.append(toRow(plugin));
+ writer.append(CharUtils.LF);
+ }
+ writer.flush();
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to generate plugin index at " + indexFile, e);
+ }
+ }
+
+ private static String toRow(InstalledPlugin file) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(file.getPluginInfo().getKey())
+ .append(",")
+ .append(file.getPluginInfo().isSonarLintSupported())
+ .append(",")
+ .append(file.getLoadedJar().getFile().getName())
+ .append("|")
+ .append(file.getLoadedJar().getMd5());
+ return sb.toString();
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/LogServerId.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/LogServerId.java
new file mode 100644
index 00000000000..5091d13b22a
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/LogServerId.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.startup;
+
+import org.picocontainer.Startable;
+import org.sonar.api.platform.Server;
+import org.sonar.api.utils.log.Loggers;
+
+public final class LogServerId implements Startable {
+
+ private final Server server;
+
+ public LogServerId(Server server) {
+ this.server = server;
+ }
+
+ @Override
+ public void start() {
+ Loggers.get(getClass()).info("Server ID: " + server.getId());
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
new file mode 100644
index 00000000000..615d91cef80
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java
@@ -0,0 +1,139 @@
+/*
+ * 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.startup;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.picocontainer.Startable;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.Metrics;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.metric.MetricToDto;
+
+import static com.google.common.collect.FluentIterable.from;
+import static com.google.common.collect.Iterables.concat;
+import static com.google.common.collect.Lists.newArrayList;
+
+public class RegisterMetrics implements Startable {
+
+ private static final Logger LOG = Loggers.get(RegisterMetrics.class);
+
+ private final DbClient dbClient;
+ private final Metrics[] metricsRepositories;
+
+ public RegisterMetrics(DbClient dbClient, Metrics[] metricsRepositories) {
+ this.dbClient = dbClient;
+ this.metricsRepositories = metricsRepositories;
+ }
+
+ /**
+ * Used when no plugin is defining Metrics
+ */
+ public RegisterMetrics(DbClient dbClient) {
+ this(dbClient, new Metrics[] {});
+ }
+
+ @Override
+ public void start() {
+ register(concat(CoreMetrics.getMetrics(), getPluginMetrics()));
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+
+ void register(Iterable<Metric> metrics) {
+ Profiler profiler = Profiler.create(LOG).startInfo("Register metrics");
+ try (DbSession session = dbClient.openSession(false)) {
+ save(session, metrics);
+ sanitizeQualityGates(session);
+ session.commit();
+ }
+ profiler.stopDebug();
+ }
+
+ private void sanitizeQualityGates(DbSession session) {
+ dbClient.gateConditionDao().deleteConditionsWithInvalidMetrics(session);
+ }
+
+ private void save(DbSession session, Iterable<Metric> metrics) {
+ Map<String, MetricDto> basesByKey = new HashMap<>();
+ for (MetricDto base : from(dbClient.metricDao().selectAll(session)).toList()) {
+ basesByKey.put(base.getKey(), base);
+ }
+
+ for (Metric metric : metrics) {
+ MetricDto dto = MetricToDto.INSTANCE.apply(metric);
+ MetricDto base = basesByKey.get(metric.getKey());
+ if (base == null) {
+ // new metric, never installed
+ dbClient.metricDao().insert(session, dto);
+ } else if (!base.isUserManaged()) {
+ // existing metric, update changes. Existing custom metrics are kept without applying changes.
+ dto.setId(base.getId());
+ dbClient.metricDao().update(session, dto);
+ }
+ basesByKey.remove(metric.getKey());
+ }
+
+ for (MetricDto nonUpdatedBase : basesByKey.values()) {
+ if (!nonUpdatedBase.isUserManaged() && dbClient.metricDao().disableCustomByKey(session, nonUpdatedBase.getKey())) {
+ LOG.info("Disable metric {} [{}]", nonUpdatedBase.getShortName(), nonUpdatedBase.getKey());
+ }
+ }
+ }
+
+ @VisibleForTesting
+ List<Metric> getPluginMetrics() {
+ List<Metric> metricsToRegister = newArrayList();
+ Map<String, Metrics> metricsByRepository = Maps.newHashMap();
+ for (Metrics metrics : metricsRepositories) {
+ checkMetrics(metricsByRepository, metrics);
+ metricsToRegister.addAll(metrics.getMetrics());
+ }
+
+ return metricsToRegister;
+ }
+
+ private void checkMetrics(Map<String, Metrics> metricsByRepository, Metrics metrics) {
+ for (Metric metric : metrics.getMetrics()) {
+ String metricKey = metric.getKey();
+ if (CoreMetrics.getMetrics().contains(metric)) {
+ throw new IllegalStateException(String.format("Metric [%s] is already defined by SonarQube", metricKey));
+ }
+ Metrics anotherRepository = metricsByRepository.get(metricKey);
+ if (anotherRepository != null) {
+ throw new IllegalStateException(String.format("Metric [%s] is already defined by the repository [%s]", metricKey, anotherRepository));
+ }
+ metricsByRepository.put(metricKey, metrics);
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java
new file mode 100644
index 00000000000..fd2ff4590cf
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java
@@ -0,0 +1,128 @@
+/*
+ * 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.startup;
+
+import org.picocontainer.Startable;
+import org.sonar.api.security.DefaultGroups;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.DefaultTemplates;
+import org.sonar.db.permission.OrganizationPermission;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+
+import java.util.Date;
+import java.util.Optional;
+
+import static java.lang.String.format;
+
+public class RegisterPermissionTemplates implements Startable {
+
+ private static final Logger LOG = Loggers.get(RegisterPermissionTemplates.class);
+ private static final String DEFAULT_TEMPLATE_UUID = "default_template";
+
+ private final DbClient dbClient;
+ private final DefaultOrganizationProvider defaultOrganizationProvider;
+
+ public RegisterPermissionTemplates(DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider) {
+ this.dbClient = dbClient;
+ this.defaultOrganizationProvider = defaultOrganizationProvider;
+ }
+
+ @Override
+ public void start() {
+ Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Register permission templates");
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ String defaultOrganizationUuid = defaultOrganizationProvider.get().getUuid();
+ Optional<DefaultTemplates> defaultTemplates = dbClient.organizationDao().getDefaultTemplates(dbSession, defaultOrganizationUuid);
+ if (!defaultTemplates.isPresent()) {
+ PermissionTemplateDto defaultTemplate = getOrInsertDefaultTemplate(dbSession, defaultOrganizationUuid);
+ dbClient.organizationDao().setDefaultTemplates(dbSession, defaultOrganizationUuid, new DefaultTemplates().setProjectUuid(defaultTemplate.getUuid()));
+ dbSession.commit();
+ }
+ }
+
+ profiler.stopDebug();
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+
+ private PermissionTemplateDto getOrInsertDefaultTemplate(DbSession dbSession, String defaultOrganizationUuid) {
+ PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().selectByUuid(dbSession, DEFAULT_TEMPLATE_UUID);
+ if (permissionTemplateDto != null) {
+ return permissionTemplateDto;
+ }
+
+ PermissionTemplateDto template = new PermissionTemplateDto()
+ .setOrganizationUuid(defaultOrganizationUuid)
+ .setName("Default template")
+ .setUuid(DEFAULT_TEMPLATE_UUID)
+ .setDescription("This permission template will be used as default when no other permission configuration is available")
+ .setCreatedAt(new Date())
+ .setUpdatedAt(new Date());
+
+ dbClient.permissionTemplateDao().insert(dbSession, template);
+ insertDefaultGroupPermissions(dbSession, template);
+ dbSession.commit();
+ return template;
+ }
+
+ private void insertDefaultGroupPermissions(DbSession dbSession, PermissionTemplateDto template) {
+ insertPermissionForAdministrators(dbSession, template);
+ insertPermissionsForDefaultGroup(dbSession, template);
+ }
+
+ private void insertPermissionForAdministrators(DbSession dbSession, PermissionTemplateDto template) {
+ Optional<GroupDto> admins = dbClient.groupDao().selectByName(dbSession, template.getOrganizationUuid(), DefaultGroups.ADMINISTRATORS);
+ if (admins.isPresent()) {
+ insertGroupPermission(dbSession, template, UserRole.ADMIN, admins.get());
+ insertGroupPermission(dbSession, template, OrganizationPermission.APPLICATION_CREATOR.getKey(), admins.get());
+ insertGroupPermission(dbSession, template, OrganizationPermission.PORTFOLIO_CREATOR.getKey(), admins.get());
+ } else {
+ LOG.error("Cannot setup default permission for group: " + DefaultGroups.ADMINISTRATORS);
+ }
+ }
+
+ private void insertPermissionsForDefaultGroup(DbSession dbSession, PermissionTemplateDto template) {
+ String organizationUuid = template.getOrganizationUuid();
+ Integer defaultGroupId = dbClient.organizationDao().getDefaultGroupId(dbSession, organizationUuid)
+ .orElseThrow(() -> new IllegalStateException(format("Default group for organization %s is not defined", organizationUuid)));
+ GroupDto defaultGroup = Optional.ofNullable(dbClient.groupDao().selectById(dbSession, defaultGroupId))
+ .orElseThrow(() -> new IllegalStateException(format("Default group with id %s for organization %s doesn't exist", defaultGroupId, organizationUuid)));
+ insertGroupPermission(dbSession, template, UserRole.USER, defaultGroup);
+ insertGroupPermission(dbSession, template, UserRole.CODEVIEWER, defaultGroup);
+ insertGroupPermission(dbSession, template, UserRole.ISSUE_ADMIN, defaultGroup);
+ insertGroupPermission(dbSession, template, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup);
+ }
+
+ private void insertGroupPermission(DbSession dbSession, PermissionTemplateDto template, String permission, GroupDto group) {
+ dbClient.permissionTemplateDao().insertGroupPermission(dbSession, template.getId(), group.getId(), permission);
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java
new file mode 100644
index 00000000000..cefa3fbb571
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.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.startup;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.sonar.api.Startable;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.System2;
+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.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.plugin.PluginDto;
+import org.sonar.server.plugins.InstalledPlugin;
+import org.sonar.server.plugins.PluginFileSystem;
+
+import static java.util.function.Function.identity;
+
+/**
+ * Take care to update the 'plugins' table at startup.
+ */
+@ServerSide
+public class RegisterPlugins implements Startable {
+
+ private static final Logger LOG = Loggers.get(RegisterPlugins.class);
+
+ private final PluginFileSystem pluginFileSystem;
+ private final DbClient dbClient;
+ private final UuidFactory uuidFactory;
+ private final System2 system;
+
+ public RegisterPlugins(PluginFileSystem pluginFileSystem, DbClient dbClient, UuidFactory uuidFactory, System2 system) {
+ this.pluginFileSystem = pluginFileSystem;
+ this.dbClient = dbClient;
+ this.uuidFactory = uuidFactory;
+ this.system = system;
+ }
+
+ @Override
+ public void start() {
+ Profiler profiler = Profiler.create(LOG).startInfo("Register plugins");
+ updateDB();
+ profiler.stopDebug();
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+
+ private void updateDB() {
+ long now = system.now();
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Map<String, PluginDto> allPreviousPluginsByKey = dbClient.pluginDao().selectAll(dbSession).stream()
+ .collect(Collectors.toMap(PluginDto::getKee, identity()));
+ for (InstalledPlugin installed : pluginFileSystem.getInstalledFiles()) {
+ PluginInfo info = installed.getPluginInfo();
+ PluginDto previousDto = allPreviousPluginsByKey.get(info.getKey());
+ if (previousDto == null) {
+ LOG.debug("Register new plugin {}", info.getKey());
+ PluginDto pluginDto = new PluginDto()
+ .setUuid(uuidFactory.create())
+ .setKee(info.getKey())
+ .setBasePluginKey(info.getBasePlugin())
+ .setFileHash(installed.getLoadedJar().getMd5())
+ .setCreatedAt(now)
+ .setUpdatedAt(now);
+ dbClient.pluginDao().insert(dbSession, pluginDto);
+ } else if (!previousDto.getFileHash().equals(installed.getLoadedJar().getMd5())) {
+ LOG.debug("Update plugin {}", info.getKey());
+ previousDto
+ .setBasePluginKey(info.getBasePlugin())
+ .setFileHash(installed.getLoadedJar().getMd5())
+ .setUpdatedAt(now);
+ dbClient.pluginDao().update(dbSession, previousDto);
+ }
+ // Don't remove uninstalled plugins, because corresponding rules and active rules are also not deleted
+ }
+ dbSession.commit();
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RenameDeprecatedPropertyKeys.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RenameDeprecatedPropertyKeys.java
new file mode 100644
index 00000000000..7be27df5c8a
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RenameDeprecatedPropertyKeys.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.startup;
+
+import com.google.common.base.Strings;
+import org.picocontainer.Startable;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.property.PropertiesDao;
+
+/**
+ * @since 3.4
+ */
+public class RenameDeprecatedPropertyKeys implements Startable {
+
+ private PropertiesDao dao;
+ private PropertyDefinitions definitions;
+
+ public RenameDeprecatedPropertyKeys(PropertiesDao dao, PropertyDefinitions definitions) {
+ this.dao = dao;
+ this.definitions = definitions;
+ }
+
+ @Override
+ public void start() {
+ Loggers.get(RenameDeprecatedPropertyKeys.class).info("Rename deprecated property keys");
+ for (PropertyDefinition definition : definitions.getAll()) {
+ if (!Strings.isNullOrEmpty(definition.deprecatedKey())) {
+ dao.renamePropertyKey(definition.deprecatedKey(), definition.key());
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/package-info.java
new file mode 100644
index 00000000000..c43f2781b47
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/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.startup;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryClient.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryClient.java
new file mode 100644
index 00000000000..719405aee62
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryClient.java
@@ -0,0 +1,92 @@
+/*
+ * 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.io.IOException;
+import okhttp3.Call;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.sonar.api.Startable;
+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 static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL;
+
+@ServerSide
+public class TelemetryClient implements Startable {
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+ private static final Logger LOG = Loggers.get(TelemetryClient.class);
+
+ private final OkHttpClient okHttpClient;
+ private final Configuration config;
+ private String serverUrl;
+
+ public TelemetryClient(OkHttpClient okHttpClient, Configuration config) {
+ this.okHttpClient = okHttpClient;
+ this.config = config;
+ }
+
+ void upload(String json) throws IOException {
+ Request request = buildHttpRequest(json);
+ execute(okHttpClient.newCall(request));
+ }
+
+ void optOut(String json) {
+ Request.Builder request = new Request.Builder();
+ request.url(serverUrl);
+ RequestBody body = RequestBody.create(JSON, json);
+ request.delete(body);
+
+ try {
+ execute(okHttpClient.newCall(request.build()));
+ } catch (IOException e) {
+ LOG.debug("Error when sending opt-out usage statistics: {}", e.getMessage());
+ }
+ }
+
+ private Request buildHttpRequest(String json) {
+ Request.Builder request = new Request.Builder();
+ request.url(serverUrl);
+ RequestBody body = RequestBody.create(JSON, json);
+ request.post(body);
+ return request.build();
+ }
+
+ private static void execute(Call call) throws IOException {
+ try (Response ignored = call.execute()) {
+ // auto close connection to avoid leaked connection
+ }
+ }
+
+ @Override
+ public void start() {
+ this.serverUrl = config.get(SONAR_TELEMETRY_URL.getKey()).orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_URL)));
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java
new file mode 100644
index 00000000000..64f44c3cc88
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java
@@ -0,0 +1,175 @@
+/*
+ * 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 com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import org.picocontainer.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.server.property.InternalProperties;
+import org.sonar.server.util.GlobalLockManager;
+
+import static org.sonar.api.utils.DateUtils.formatDate;
+import static org.sonar.api.utils.DateUtils.parseDate;
+import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_ENABLE;
+import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_FREQUENCY_IN_SECONDS;
+import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL;
+import static org.sonar.server.telemetry.TelemetryDataJsonWriter.writeTelemetryData;
+
+@ServerSide
+public class TelemetryDaemon implements Startable {
+ private static final String THREAD_NAME_PREFIX = "sq-telemetry-service-";
+ private static final int SEVEN_DAYS = 7 * 24 * 60 * 60 * 1_000;
+ private static final String I_PROP_LAST_PING = "telemetry.lastPing";
+ private static final String I_PROP_OPT_OUT = "telemetry.optOut";
+ private static final String LOCK_NAME = "TelemetryStat";
+ private static final Logger LOG = Loggers.get(TelemetryDaemon.class);
+ private static final String LOCK_DELAY_SEC = "sonar.telemetry.lock.delay";
+
+ private final TelemetryDataLoader dataLoader;
+ private final TelemetryClient telemetryClient;
+ private final GlobalLockManager lockManager;
+ private final Configuration config;
+ private final InternalProperties internalProperties;
+ private final System2 system2;
+
+ private ScheduledExecutorService executorService;
+
+ public TelemetryDaemon(TelemetryDataLoader dataLoader, TelemetryClient telemetryClient, Configuration config,
+ InternalProperties internalProperties, GlobalLockManager lockManager, System2 system2) {
+ this.dataLoader = dataLoader;
+ this.telemetryClient = telemetryClient;
+ this.config = config;
+ this.internalProperties = internalProperties;
+ this.lockManager = lockManager;
+ this.system2 = system2;
+ }
+
+ @Override
+ public void start() {
+ boolean isTelemetryActivated = config.getBoolean(SONAR_TELEMETRY_ENABLE.getKey())
+ .orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_URL.getKey())));
+ boolean hasOptOut = internalProperties.read(I_PROP_OPT_OUT).isPresent();
+ if (!isTelemetryActivated && !hasOptOut) {
+ optOut();
+ internalProperties.write(I_PROP_OPT_OUT, String.valueOf(system2.now()));
+ LOG.info("Sharing of SonarQube statistics is disabled.");
+ }
+ if (isTelemetryActivated && hasOptOut) {
+ internalProperties.write(I_PROP_OPT_OUT, null);
+ }
+ if (!isTelemetryActivated) {
+ return;
+ }
+ LOG.info("Sharing of SonarQube statistics is enabled.");
+ executorService = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
+ int frequencyInSeconds = frequency();
+ executorService.scheduleWithFixedDelay(telemetryCommand(), frequencyInSeconds, frequencyInSeconds, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void stop() {
+ try {
+ if (executorService == null) {
+ return;
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private static ThreadFactory newThreadFactory() {
+ return new ThreadFactoryBuilder()
+ .setNameFormat(THREAD_NAME_PREFIX + "%d")
+ .setPriority(Thread.MIN_PRIORITY)
+ .build();
+ }
+
+ private Runnable telemetryCommand() {
+ return () -> {
+ try {
+
+ if (!lockManager.tryLock(LOCK_NAME, lockDuration())) {
+ return;
+ }
+
+ long now = system2.now();
+ if (shouldUploadStatistics(now)) {
+ uploadStatistics();
+ internalProperties.write(I_PROP_LAST_PING, String.valueOf(startOfDay(now)));
+ }
+ } catch (Exception e) {
+ LOG.debug("Error while checking SonarQube statistics: {}", e.getMessage());
+ }
+ // do not check at start up to exclude test instance which are not up for a long time
+ };
+ }
+
+ private void optOut() {
+ StringWriter json = new StringWriter();
+ try (JsonWriter writer = JsonWriter.of(json)) {
+ writer.beginObject();
+ writer.prop("id", dataLoader.loadServerId());
+ writer.endObject();
+ }
+ telemetryClient.optOut(json.toString());
+ }
+
+ private void uploadStatistics() throws IOException {
+ TelemetryData statistics = dataLoader.load();
+ StringWriter jsonString = new StringWriter();
+ try (JsonWriter json = JsonWriter.of(jsonString)) {
+ writeTelemetryData(json, statistics);
+ }
+ telemetryClient.upload(jsonString.toString());
+ }
+
+ private boolean shouldUploadStatistics(long now) {
+ Optional<Long> lastPing = internalProperties.read(I_PROP_LAST_PING).map(Long::valueOf);
+ return !lastPing.isPresent() || now - lastPing.get() >= SEVEN_DAYS;
+ }
+
+ private static long startOfDay(long now) {
+ return parseDate(formatDate(new Date(now))).getTime();
+ }
+
+ private int frequency() {
+ return config.getInt(SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getKey())
+ .orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_FREQUENCY_IN_SECONDS)));
+ }
+
+ private int lockDuration() {
+ return config.getInt(LOCK_DELAY_SEC).orElse(60);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java
new file mode 100644
index 00000000000..b31f2b30678
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java
@@ -0,0 +1,131 @@
+/*
+ * 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.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.platform.Server;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.platform.PlatformEditionProvider;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginRepository;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.measure.SumNclocDbQuery;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.measure.index.ProjectMeasuresIndex;
+import org.sonar.server.measure.index.ProjectMeasuresStatistics;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.property.InternalProperties;
+import org.sonar.server.telemetry.TelemetryData.Database;
+import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserQuery;
+
+import static java.util.Optional.ofNullable;
+
+@ServerSide
+public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
+ private final Server server;
+ private final DbClient dbClient;
+ private final PluginRepository pluginRepository;
+ private final UserIndex userIndex;
+ private final ProjectMeasuresIndex projectMeasuresIndex;
+ private final PlatformEditionProvider editionProvider;
+ private final DefaultOrganizationProvider defaultOrganizationProvider;
+ private final InternalProperties internalProperties;
+ @CheckForNull
+ private final LicenseReader licenseReader;
+
+ public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository, UserIndex userIndex, ProjectMeasuresIndex projectMeasuresIndex,
+ PlatformEditionProvider editionProvider, DefaultOrganizationProvider defaultOrganizationProvider, InternalProperties internalProperties) {
+ this(server, dbClient, pluginRepository, userIndex, projectMeasuresIndex, editionProvider, defaultOrganizationProvider, internalProperties, null);
+ }
+
+ public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository, UserIndex userIndex, ProjectMeasuresIndex projectMeasuresIndex,
+ PlatformEditionProvider editionProvider, DefaultOrganizationProvider defaultOrganizationProvider, InternalProperties internalProperties,
+ @Nullable LicenseReader licenseReader) {
+ this.server = server;
+ this.dbClient = dbClient;
+ this.pluginRepository = pluginRepository;
+ this.userIndex = userIndex;
+ this.projectMeasuresIndex = projectMeasuresIndex;
+ this.editionProvider = editionProvider;
+ this.defaultOrganizationProvider = defaultOrganizationProvider;
+ this.licenseReader = licenseReader;
+ this.internalProperties = internalProperties;
+ }
+
+ private static Database loadDatabaseMetadata(DbSession dbSession) {
+ try {
+ DatabaseMetaData metadata = dbSession.getConnection().getMetaData();
+ return new Database(metadata.getDatabaseProductName(), metadata.getDatabaseProductVersion());
+ } catch (SQLException e) {
+ throw new IllegalStateException("Fail to get DB metadata", e);
+ }
+ }
+
+ @Override
+ public TelemetryData load() {
+ TelemetryData.Builder data = TelemetryData.builder();
+
+ data.setServerId(server.getId());
+ data.setVersion(server.getVersion());
+ data.setEdition(editionProvider.get());
+ ofNullable(licenseReader)
+ .flatMap(reader -> licenseReader.read())
+ .ifPresent(license -> data.setLicenseType(license.getType()));
+ Function<PluginInfo, String> getVersion = plugin -> plugin.getVersion() == null ? "undefined" : plugin.getVersion().getName();
+ Map<String, String> plugins = pluginRepository.getPluginInfos().stream().collect(MoreCollectors.uniqueIndex(PluginInfo::getKey, getVersion));
+ data.setPlugins(plugins);
+ long userCount = userIndex.search(UserQuery.builder().build(), new SearchOptions().setLimit(1)).getTotal();
+ data.setUserCount(userCount);
+ ProjectMeasuresStatistics projectMeasuresStatistics = projectMeasuresIndex.searchTelemetryStatistics();
+ data.setProjectMeasuresStatistics(projectMeasuresStatistics);
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ data.setDatabase(loadDatabaseMetadata(dbSession));
+ data.setUsingBranches(dbClient.branchDao().hasNonMainBranches(dbSession));
+ SumNclocDbQuery query = SumNclocDbQuery.builder()
+ .setOnlyPrivateProjects(false)
+ .setOrganizationUuid(defaultOrganizationProvider.get().getUuid())
+ .build();
+ data.setNcloc(dbClient.liveMeasureDao().sumNclocOfBiggestLongLivingBranch(dbSession, query));
+ }
+
+ Optional<String> installationDateProperty = internalProperties.read(InternalProperties.INSTALLATION_DATE);
+ if (installationDateProperty.isPresent()) {
+ data.setInstallationDate(Long.valueOf(installationDateProperty.get()));
+ }
+ Optional<String> installationVersionProperty = internalProperties.read(InternalProperties.INSTALLATION_VERSION);
+ data.setInstallationVersion(installationVersionProperty.orElse(null));
+
+ return data.build();
+ }
+
+ @Override
+ public String loadServerId() {
+ return server.getId();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/package-info.java
new file mode 100644
index 00000000000..b85d360841b
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/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.telemetry;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/UpdateCenterModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/UpdateCenterModule.java
new file mode 100644
index 00000000000..d3f07169f61
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/UpdateCenterModule.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.updatecenter;
+
+import org.sonar.core.platform.Module;
+import org.sonar.server.plugins.UpdateCenterClient;
+import org.sonar.server.plugins.UpdateCenterMatrixFactory;
+
+public class UpdateCenterModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ UpdateCenterClient.class,
+ UpdateCenterMatrixFactory.class);
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/package-info.java
new file mode 100644
index 00000000000..b89cb43f66a
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/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.updatecenter;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/ClassLoaderUtils.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/ClassLoaderUtils.java
new file mode 100644
index 00000000000..9e273d66307
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/ClassLoaderUtils.java
@@ -0,0 +1,114 @@
+/*
+ * 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.Throwables;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.function.Predicate;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class ClassLoaderUtils {
+
+ private ClassLoaderUtils() {
+ // only static methods
+ }
+
+ /**
+ * Finds files within a given directory and its subdirectories
+ *
+ * @param classLoader
+ * @param rootPath the root directory, for example org/sonar/sqale
+ * @return a list of relative paths, for example {"org/sonar/sqale/foo/bar.txt}. Never null.
+ */
+ public static Collection<String> listFiles(ClassLoader classLoader, String rootPath) {
+ return listResources(classLoader, rootPath, path -> !StringUtils.endsWith(path, "/"));
+ }
+
+ /**
+ * Finds directories and files within a given directory and its subdirectories.
+ *
+ * @param classLoader
+ * @param rootPath the root directory, for example org/sonar/sqale, or a file in this root directory, for example org/sonar/sqale/index.txt
+ * @param predicate
+ * @return a list of relative paths, for example {"org/sonar/sqale", "org/sonar/sqale/foo", "org/sonar/sqale/foo/bar.txt}. Never null.
+ */
+ public static Collection<String> listResources(ClassLoader classLoader, String rootPath, Predicate<String> predicate) {
+ String jarPath = null;
+ JarFile jar = null;
+ try {
+ Collection<String> paths = new ArrayList<>();
+ URL root = classLoader.getResource(rootPath);
+ if (root != null) {
+ checkJarFile(root);
+
+ // Path of the root directory
+ // Examples :
+ // org/sonar/sqale/index.txt -> rootDirectory is org/sonar/sqale
+ // org/sonar/sqale/ -> rootDirectory is org/sonar/sqale
+ // org/sonar/sqale -> rootDirectory is org/sonar/sqale
+ String rootDirectory = rootPath;
+ if (StringUtils.substringAfterLast(rootPath, "/").indexOf('.') >= 0) {
+ rootDirectory = StringUtils.substringBeforeLast(rootPath, "/");
+ }
+ // strip out only the JAR file
+ jarPath = root.getPath().substring(5, root.getPath().indexOf('!'));
+ jar = new JarFile(URLDecoder.decode(jarPath, UTF_8.name()));
+ Enumeration<JarEntry> entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ String name = entries.nextElement().getName();
+ if (name.startsWith(rootDirectory) && predicate.test(name)) {
+ paths.add(name);
+ }
+ }
+ }
+ return paths;
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ } finally {
+ closeJar(jar, jarPath);
+ }
+ }
+
+ private static void closeJar(@Nullable JarFile jar, String jarPath) {
+ if (jar != null) {
+ try {
+ jar.close();
+ } catch (Exception e) {
+ Loggers.get(ClassLoaderUtils.class).error("Fail to close JAR file: " + jarPath, e);
+ }
+ }
+ }
+
+ private static void checkJarFile(URL root) {
+ if (!"jar".equals(root.getProtocol())) {
+ throw new IllegalStateException("Unsupported protocol: " + root.getProtocol());
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/DateCollector.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/DateCollector.java
new file mode 100644
index 00000000000..446f1a499ab
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/DateCollector.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.util;
+
+import javax.annotation.Nullable;
+
+import java.util.Date;
+
+public class DateCollector {
+
+ private long maxDate = 0L;
+
+ public void add(@Nullable Date d) {
+ if (d != null) {
+ add(d.getTime());
+ }
+ }
+
+ public void add(long date) {
+ maxDate = Math.max(maxDate, date);
+ }
+
+ /**
+ * The most recent collected date. Value is zero if no dates were collected.
+ */
+ public long getMax() {
+ return maxDate;
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/TempFolderCleaner.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/TempFolderCleaner.java
new file mode 100644
index 00000000000..812af0edb85
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/TempFolderCleaner.java
@@ -0,0 +1,53 @@
+/*
+ * 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.Startable;
+import org.sonar.api.impl.utils.DefaultTempFolder;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.TempFolder;
+
+@ServerSide
+public class TempFolderCleaner implements Startable {
+
+ private TempFolder defaultTempFolder;
+
+ public TempFolderCleaner(TempFolder defaultTempFolder) {
+ this.defaultTempFolder = defaultTempFolder;
+ }
+
+ /**
+ * This method should not be renamed. It follows the naming convention
+ * defined by IoC container.
+ */
+ @Override
+ public void start() {
+ // Nothing to do
+ }
+
+ /**
+ * This method should not be renamed. It follows the naming convention
+ * defined by IoC container.
+ */
+ @Override
+ public void stop() {
+ ((DefaultTempFolder) defaultTempFolder).clean();
+ }
+}
diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/package-info.java
new file mode 100644
index 00000000000..d02f184b834
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/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.util;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-core/src/main/resources/build.properties b/server/sonar-webserver-core/src/main/resources/build.properties
new file mode 100644
index 00000000000..c73bb2bc3ae
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/resources/build.properties
@@ -0,0 +1 @@
+Implementation-Build=@buildNumber@ \ No newline at end of file
diff --git a/server/sonar-webserver-core/src/main/resources/com/sonar/sqale/technical-debt-model.xml b/server/sonar-webserver-core/src/main/resources/com/sonar/sqale/technical-debt-model.xml
new file mode 100644
index 00000000000..4e3277a44b6
--- /dev/null
+++ b/server/sonar-webserver-core/src/main/resources/com/sonar/sqale/technical-debt-model.xml
@@ -0,0 +1,206 @@
+<sqale>
+ <chc>
+ <key>REUSABILITY</key>
+ <name>Reusability</name>
+ <chc>
+ <key>MODULARITY</key>
+ <name>Modularity</name>
+ </chc>
+ <chc>
+ <key>REUSABILITY_COMPLIANCE</key>
+ <name>Reusability Compliance</name>
+ </chc>
+ <chc>
+ <key>TRANSPORTABILITY</key>
+ <name>Transportability</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>PORTABILITY</key>
+ <name>Portability</name>
+ <chc>
+ <key>COMPILER_RELATED_PORTABILITY</key>
+ <name>Compiler</name>
+ </chc>
+ <chc>
+ <key>HARDWARE_RELATED_PORTABILITY</key>
+ <name>Hardware</name>
+ </chc>
+ <chc>
+ <key>LANGUAGE_RELATED_PORTABILITY</key>
+ <name>Language</name>
+ </chc>
+ <chc>
+ <key>OS_RELATED_PORTABILITY</key>
+ <name>OS</name>
+ </chc>
+ <chc>
+ <key>PORTABILITY_COMPLIANCE</key>
+ <name>Portability Compliance</name>
+ </chc>
+ <chc>
+ <key>SOFTWARE_RELATED_PORTABILITY</key>
+ <name>Software</name>
+ </chc>
+ <chc>
+ <key>TIME_ZONE_RELATED_PORTABILITY</key>
+ <name>Time zone</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>MAINTAINABILITY</key>
+ <name>Maintainability</name>
+ <chc>
+ <key>MAINTAINABILITY_COMPLIANCE</key>
+ <name>Maintainability Compliance</name>
+ </chc>
+ <chc>
+ <key>READABILITY</key>
+ <name>Readability</name>
+ </chc>
+ <chc>
+ <key>UNDERSTANDABILITY</key>
+ <name>Understandability</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>SECURITY</key>
+ <name>Security</name>
+ <chc>
+ <key>API_ABUSE</key>
+ <name>API abuse</name>
+ </chc>
+ <chc>
+ <key>ERRORS</key>
+ <name>Errors</name>
+ </chc>
+ <chc>
+ <key>INPUT_VALIDATION_AND_REPRESENTATION</key>
+ <name>Input validation and representation</name>
+ </chc>
+ <chc>
+ <key>SECURITY_COMPLIANCE</key>
+ <name>Security Compliance</name>
+ </chc>
+ <chc>
+ <key>SECURITY_FEATURES</key>
+ <name>Security features</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <chc>
+ <key>USABILITY_ACCESSIBILITY</key>
+ <name>Accessibility</name>
+ </chc>
+ <chc>
+ <key>USABILITY_EASE_OF_USE</key>
+ <name>Ease of Use</name>
+ </chc>
+ <chc>
+ <key>USABILITY_COMPLIANCE</key>
+ <name>Usability Compliance</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>CPU_EFFICIENCY</key>
+ <name>Processor use</name>
+ </chc>
+ <chc>
+ <key>EFFICIENCY_COMPLIANCE</key>
+ <name>Efficiency Compliance</name>
+ </chc>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ </chc>
+ <chc>
+ <key>NETWORK_USE</key>
+ <name>Network use</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>CHANGEABILITY</key>
+ <name>Changeability</name>
+ <chc>
+ <key>ARCHITECTURE_CHANGEABILITY</key>
+ <name>Architecture</name>
+ </chc>
+ <chc>
+ <key>CHANGEABILITY_COMPLIANCE</key>
+ <name>Changeability Compliance</name>
+ </chc>
+ <chc>
+ <key>DATA_CHANGEABILITY</key>
+ <name>Data</name>
+ </chc>
+ <chc>
+ <key>LOGIC_CHANGEABILITY</key>
+ <name>Logic</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>RELIABILITY</key>
+ <name>Reliability</name>
+ <chc>
+ <key>ARCHITECTURE_RELIABILITY</key>
+ <name>Architecture</name>
+ </chc>
+ <chc>
+ <key>DATA_RELIABILITY</key>
+ <name>Data</name>
+ </chc>
+ <chc>
+ <key>EXCEPTION_HANDLING</key>
+ <name>Exception handling</name>
+ </chc>
+ <chc>
+ <key>FAULT_TOLERANCE</key>
+ <name>Fault tolerance</name>
+ </chc>
+ <chc>
+ <key>INSTRUCTION_RELIABILITY</key>
+ <name>Instruction</name>
+ </chc>
+ <chc>
+ <key>LOGIC_RELIABILITY</key>
+ <name>Logic</name>
+ </chc>
+ <chc>
+ <key>RELIABILITY_COMPLIANCE</key>
+ <name>Reliability Compliance</name>
+ </chc>
+ <chc>
+ <key>RESOURCE_RELIABILITY</key>
+ <name>Resource</name>
+ </chc>
+ <chc>
+ <key>SYNCHRONIZATION_RELIABILITY</key>
+ <name>Synchronization</name>
+ </chc>
+ <chc>
+ <key>UNIT_TESTS</key>
+ <name>Unit tests coverage</name>
+ </chc>
+ </chc>
+ <chc>
+ <key>TESTABILITY</key>
+ <name>Testability</name>
+ <chc>
+ <key>INTEGRATION_TESTABILITY</key>
+ <name>Integration level</name>
+ </chc>
+ <chc>
+ <key>TESTABILITY_COMPLIANCE</key>
+ <name>Testability Compliance</name>
+ </chc>
+ <chc>
+ <key>UNIT_TESTABILITY</key>
+ <name>Unit level</name>
+ </chc>
+ </chc>
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java
new file mode 100644
index 00000000000..b5612766341
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java
@@ -0,0 +1,578 @@
+/*
+ * 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 ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.AppenderBase;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.OutputStreamAppender;
+import ch.qos.logback.core.encoder.Encoder;
+import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
+import ch.qos.logback.core.joran.spi.JoranException;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.stream.Stream;
+import org.junit.AfterClass;
+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.process.Props;
+import org.sonar.process.logging.LogbackHelper;
+import org.sonar.process.logging.LogbackJsonLayout;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.slf4j.Logger.ROOT_LOGGER_NAME;
+import static org.sonar.process.ProcessProperties.Property.PATH_LOGS;
+
+public class WebServerProcessLoggingTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private File logDir;
+ private Props props = new Props(new Properties());
+ private WebServerProcessLogging underTest = new WebServerProcessLogging();
+
+ @Before
+ public void setUp() throws IOException {
+ logDir = temp.newFolder();
+ props.set(PATH_LOGS.getKey(), logDir.getAbsolutePath());
+ }
+
+ @AfterClass
+ public static void resetLogback() throws JoranException {
+ new LogbackHelper().resetFromXml("/logback-test.xml");
+ }
+
+ @Test
+ public void do_not_log_to_console() {
+ LoggerContext ctx = underTest.configure(props);
+
+ Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME);
+ Appender appender = root.getAppender("CONSOLE");
+ assertThat(appender).isNull();
+ }
+
+ @Test
+ public void check_level_of_jul() throws IOException {
+ Props props = new Props(new Properties());
+ File dir = temp.newFolder();
+ props.set(PATH_LOGS.getKey(), dir.getAbsolutePath());
+ props.set("sonar.log.level.web", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ MemoryAppender memoryAppender = new MemoryAppender();
+ memoryAppender.start();
+ ctx.getLogger(ROOT_LOGGER_NAME).addAppender(memoryAppender);
+
+ java.util.logging.Logger logger = java.util.logging.Logger.getLogger("com.ms.sqlserver.jdbc.DTV");
+ logger.finest("Test");
+ memoryAppender.stop();
+ assertThat(memoryAppender.getLogs()).hasSize(1);
+ }
+
+ @Test
+ public void startup_logger_prints_to_only_to_system_out() {
+ LoggerContext ctx = underTest.configure(props);
+
+ Logger startup = ctx.getLogger("startup");
+ assertThat(startup.isAdditive()).isFalse();
+ Appender appender = startup.getAppender("CONSOLE");
+ assertThat(appender).isInstanceOf(ConsoleAppender.class);
+ ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
+ assertThat(consoleAppender.getTarget()).isEqualTo("System.out");
+ assertThat(consoleAppender.getEncoder()).isInstanceOf(PatternLayoutEncoder.class);
+ PatternLayoutEncoder patternEncoder = (PatternLayoutEncoder) consoleAppender.getEncoder();
+ assertThat(patternEncoder.getPattern()).isEqualTo("%d{yyyy.MM.dd HH:mm:ss} %-5level app[][%logger{20}] %msg%n");
+ }
+
+ @Test
+ public void log_to_web_file() {
+ LoggerContext ctx = underTest.configure(props);
+
+ Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME);
+ Appender<ILoggingEvent> appender = root.getAppender("file_web");
+ assertThat(appender).isInstanceOf(FileAppender.class);
+ FileAppender fileAppender = (FileAppender) appender;
+ assertThat(fileAppender.getFile()).isEqualTo(new File(logDir, "web.log").getAbsolutePath());
+ assertThat(fileAppender.getEncoder()).isInstanceOf(PatternLayoutEncoder.class);
+ PatternLayoutEncoder encoder = (PatternLayoutEncoder) fileAppender.getEncoder();
+ assertThat(encoder.getPattern()).isEqualTo("%d{yyyy.MM.dd HH:mm:ss} %-5level web[%X{HTTP_REQUEST_ID}][%logger{20}] %msg%n");
+ }
+
+ @Test
+ public void default_level_for_root_logger_is_INFO() {
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyRootLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_global_property() {
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyRootLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_web_property() {
+ props.set("sonar.log.level.web", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyRootLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void root_logger_level_is_configured_from_web_property_over_global_property() {
+ props.set("sonar.log.level", "TRACE");
+ props.set("sonar.log.level.web", "DEBUG");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyRootLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_web_property_and_is_case_insensitive() {
+ props.set("sonar.log.level.web", "debug");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyRootLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void sql_logger_level_changes_with_global_property_and_is_case_insensitive() {
+ props.set("sonar.log.level", "InFO");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifySqlLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void sql_logger_level_changes_with_web_property_and_is_case_insensitive() {
+ props.set("sonar.log.level.web", "TrACe");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifySqlLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void sql_logger_level_changes_with_web_sql_property_and_is_case_insensitive() {
+ props.set("sonar.log.level.web.sql", "debug");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifySqlLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void sql_logger_level_is_configured_from_web_sql_property_over_web_property() {
+ props.set("sonar.log.level.web.sql", "debug");
+ props.set("sonar.log.level.web", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifySqlLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void sql_logger_level_is_configured_from_web_sql_property_over_global_property() {
+ props.set("sonar.log.level.web.sql", "debug");
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifySqlLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void sql_logger_level_is_configured_from_web_property_over_global_property() {
+ props.set("sonar.log.level.web", "debug");
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifySqlLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void es_logger_level_changes_with_global_property_and_is_case_insensitive() {
+ props.set("sonar.log.level", "InFO");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyEsLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void es_logger_level_changes_with_web_property_and_is_case_insensitive() {
+ props.set("sonar.log.level.web", "TrACe");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyEsLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void es_logger_level_changes_with_web_es_property_and_is_case_insensitive() {
+ props.set("sonar.log.level.web.es", "debug");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyEsLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void es_logger_level_is_configured_from_web_es_property_over_web_property() {
+ props.set("sonar.log.level.web.es", "debug");
+ props.set("sonar.log.level.web", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyEsLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void es_logger_level_is_configured_from_web_es_property_over_global_property() {
+ props.set("sonar.log.level.web.es", "debug");
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyEsLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void es_logger_level_is_configured_from_web_property_over_global_property() {
+ props.set("sonar.log.level.web", "debug");
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyEsLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void jmx_logger_level_changes_with_global_property_and_is_case_insensitive() {
+ props.set("sonar.log.level", "InFO");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyJmxLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void jmx_logger_level_changes_with_jmx_property_and_is_case_insensitive() {
+ props.set("sonar.log.level.web", "TrACe");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyJmxLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void jmx_logger_level_changes_with_web_jmx_property_and_is_case_insensitive() {
+ props.set("sonar.log.level.web.jmx", "debug");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyJmxLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void jmx_logger_level_is_configured_from_web_jmx_property_over_web_property() {
+ props.set("sonar.log.level.web.jmx", "debug");
+ props.set("sonar.log.level.web", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyJmxLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void jmx_logger_level_is_configured_from_web_jmx_property_over_global_property() {
+ props.set("sonar.log.level.web.jmx", "debug");
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyJmxLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void jmx_logger_level_is_configured_from_web_property_over_global_property() {
+ props.set("sonar.log.level.web", "debug");
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure(props);
+
+ verifyJmxLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void root_logger_level_defaults_to_INFO_if_web_property_has_invalid_value() {
+ props.set("sonar.log.level.web", "DodoDouh!");
+
+ LoggerContext ctx = underTest.configure(props);
+ verifyRootLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void sql_logger_level_defaults_to_INFO_if_web_sql_property_has_invalid_value() {
+ props.set("sonar.log.level.web.sql", "DodoDouh!");
+
+ LoggerContext ctx = underTest.configure(props);
+ verifySqlLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void es_logger_level_defaults_to_INFO_if_web_es_property_has_invalid_value() {
+ props.set("sonar.log.level.web.es", "DodoDouh!");
+
+ LoggerContext ctx = underTest.configure(props);
+ verifyEsLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void jmx_loggers_level_defaults_to_INFO_if_wedb_jmx_property_has_invalid_value() {
+ props.set("sonar.log.level.web.jmx", "DodoDouh!");
+
+ LoggerContext ctx = underTest.configure(props);
+ verifyJmxLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void fail_with_IAE_if_global_property_unsupported_level() {
+ props.set("sonar.log.level", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure(props);
+ }
+
+ @Test
+ public void fail_with_IAE_if_web_property_unsupported_level() {
+ props.set("sonar.log.level.web", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level.web is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure(props);
+ }
+
+ @Test
+ public void fail_with_IAE_if_web_sql_property_unsupported_level() {
+ props.set("sonar.log.level.web.sql", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level.web.sql is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure(props);
+ }
+
+ @Test
+ public void fail_with_IAE_if_web_es_property_unsupported_level() {
+ props.set("sonar.log.level.web.es", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level.web.es is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure(props);
+ }
+
+ @Test
+ public void fail_with_IAE_if_web_jmx_property_unsupported_level() {
+ props.set("sonar.log.level.web.jmx", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level.web.jmx is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure(props);
+ }
+
+ @Test
+ public void configure_defines_hardcoded_levels() {
+ LoggerContext context = underTest.configure(props);
+
+ verifyImmutableLogLevels(context);
+ }
+
+ @Test
+ public void configure_defines_hardcoded_levels_unchanged_by_global_property() {
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext context = underTest.configure(props);
+
+ verifyImmutableLogLevels(context);
+ }
+
+ @Test
+ public void configure_defines_hardcoded_levels_unchanged_by_ce_property() {
+ props.set("sonar.log.level.ce", "TRACE");
+
+ LoggerContext context = underTest.configure(props);
+
+ verifyImmutableLogLevels(context);
+ }
+
+ @Test
+ public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_not_set() {
+ LoggerContext context = underTest.configure(props);
+
+ verifyTomcatLoggersLogLevelsOff(context);
+ }
+
+ @Test
+ public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_INFO() {
+ props.set("sonar.log.level", "INFO");
+
+ LoggerContext context = underTest.configure(props);
+
+ verifyTomcatLoggersLogLevelsOff(context);
+ }
+
+ @Test
+ public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_DEBUG() {
+ props.set("sonar.log.level", "DEBUG");
+
+ LoggerContext context = underTest.configure(props);
+
+ verifyTomcatLoggersLogLevelsOff(context);
+ }
+
+ @Test
+ public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_TRACE() {
+ props.set("sonar.log.level", "TRACE");
+
+ LoggerContext context = underTest.configure(props);
+
+ assertThat(context.getLogger("org.apache.catalina.core.ContainerBase").getLevel()).isNull();
+ assertThat(context.getLogger("org.apache.catalina.core.StandardContext").getLevel()).isNull();
+ assertThat(context.getLogger("org.apache.catalina.core.StandardService").getLevel()).isNull();
+ }
+
+ @Test
+ public void configure_turns_off_some_MsSQL_driver_logger() {
+ LoggerContext context = underTest.configure(props);
+
+ Stream.of("com.microsoft.sqlserver.jdbc.internals",
+ "com.microsoft.sqlserver.jdbc.ResultSet",
+ "com.microsoft.sqlserver.jdbc.Statement",
+ "com.microsoft.sqlserver.jdbc.Connection")
+ .forEach(loggerName -> assertThat(context.getLogger(loggerName).getLevel()).isEqualTo(Level.OFF));
+ }
+
+ @Test
+ public void use_json_output() {
+ props.set("sonar.log.useJsonOutput", "true");
+
+ LoggerContext context = underTest.configure(props);
+
+ Logger rootLogger = context.getLogger(ROOT_LOGGER_NAME);
+ OutputStreamAppender appender = (OutputStreamAppender)rootLogger.getAppender("file_web");
+ Encoder<ILoggingEvent> encoder = appender.getEncoder();
+ assertThat(encoder).isInstanceOf(LayoutWrappingEncoder.class);
+ assertThat(((LayoutWrappingEncoder)encoder).getLayout()).isInstanceOf(LogbackJsonLayout.class);
+ }
+
+ private void verifyRootLogLevel(LoggerContext ctx, Level expected) {
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ assertThat(rootLogger.getLevel()).isEqualTo(expected);
+ }
+
+ private void verifySqlLogLevel(LoggerContext ctx, Level expected) {
+ assertThat(ctx.getLogger("sql").getLevel()).isEqualTo(expected);
+ }
+
+ private void verifyEsLogLevel(LoggerContext ctx, Level expected) {
+ assertThat(ctx.getLogger("es").getLevel()).isEqualTo(expected);
+ }
+
+ private void verifyJmxLogLevel(LoggerContext ctx, Level expected) {
+ assertThat(ctx.getLogger("javax.management.remote.timeout").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("javax.management.remote.misc").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("javax.management.remote.rmi").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("javax.management.mbeanserver").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("sun.rmi.loader").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("sun.rmi.transport.tcp").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("sun.rmi.transport.misc").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("sun.rmi.server.call").getLevel()).isEqualTo(expected);
+ assertThat(ctx.getLogger("sun.rmi.dgc").getLevel()).isEqualTo(expected);
+ }
+
+ private void verifyImmutableLogLevels(LoggerContext ctx) {
+ assertThat(ctx.getLogger("org.apache.ibatis").getLevel()).isEqualTo(Level.WARN);
+ assertThat(ctx.getLogger("java.sql").getLevel()).isEqualTo(Level.WARN);
+ assertThat(ctx.getLogger("java.sql.ResultSet").getLevel()).isEqualTo(Level.WARN);
+ assertThat(ctx.getLogger("org.elasticsearch").getLevel()).isEqualTo(Level.INFO);
+ assertThat(ctx.getLogger("org.elasticsearch.node").getLevel()).isEqualTo(Level.INFO);
+ assertThat(ctx.getLogger("org.elasticsearch.http").getLevel()).isEqualTo(Level.INFO);
+ assertThat(ctx.getLogger("ch.qos.logback").getLevel()).isEqualTo(Level.WARN);
+ assertThat(ctx.getLogger("org.apache.catalina").getLevel()).isEqualTo(Level.INFO);
+ assertThat(ctx.getLogger("org.apache.coyote").getLevel()).isEqualTo(Level.INFO);
+ assertThat(ctx.getLogger("org.apache.jasper").getLevel()).isEqualTo(Level.INFO);
+ assertThat(ctx.getLogger("org.apache.tomcat").getLevel()).isEqualTo(Level.INFO);
+ }
+
+ private void verifyTomcatLoggersLogLevelsOff(LoggerContext context) {
+ assertThat(context.getLogger("org.apache.catalina.core.ContainerBase").getLevel()).isEqualTo(Level.OFF);
+ assertThat(context.getLogger("org.apache.catalina.core.StandardContext").getLevel()).isEqualTo(Level.OFF);
+ assertThat(context.getLogger("org.apache.catalina.core.StandardService").getLevel()).isEqualTo(Level.OFF);
+ }
+
+ public static class MemoryAppender extends AppenderBase<ILoggingEvent> {
+ private static final List<ILoggingEvent> LOGS = new ArrayList<>();
+
+ @Override
+ protected void append(ILoggingEvent eventObject) {
+ LOGS.add(eventObject);
+ }
+
+ public List<ILoggingEvent> getLogs() {
+ return ImmutableList.copyOf(LOGS);
+ }
+
+ public void clear() {
+ LOGS.clear();
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelPluginRepositoryTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelPluginRepositoryTest.java
new file mode 100644
index 00000000000..5275d006954
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelPluginRepositoryTest.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.debt;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.io.Resources;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.Plugin;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DebtModelPluginRepositoryTest {
+
+ private static final String TEST_XML_PREFIX_PATH = "org/sonar/server/debt/DebtModelPluginRepositoryTest/";
+
+ DebtModelPluginRepository underTest;
+
+ @Test
+ public void test_component_initialization() {
+ // we do have the "csharp-model.xml" file in src/test/resources
+ PluginInfo csharpPluginMetadata = new PluginInfo("csharp");
+
+ // but we don' have the "php-model.xml" one
+ PluginInfo phpPluginMetadata = new PluginInfo("php");
+
+ PluginRepository repository = mock(PluginRepository.class);
+ when(repository.getPluginInfos()).thenReturn(Lists.newArrayList(csharpPluginMetadata, phpPluginMetadata));
+ FakePlugin fakePlugin = new FakePlugin();
+ when(repository.getPluginInstance(anyString())).thenReturn(fakePlugin);
+ underTest = new DebtModelPluginRepository(repository, TEST_XML_PREFIX_PATH);
+
+ // when
+ underTest.start();
+
+ // assert
+ Collection<String> contributingPluginList = underTest.getContributingPluginList();
+ assertThat(contributingPluginList.size()).isEqualTo(2);
+ assertThat(contributingPluginList).containsOnly("technical-debt", "csharp");
+ }
+
+ @Test
+ public void contributing_plugin_list() {
+ initModel();
+ Collection<String> contributingPluginList = underTest.getContributingPluginList();
+ assertThat(contributingPluginList.size()).isEqualTo(2);
+ assertThat(contributingPluginList).contains("csharp", "java");
+ }
+
+ @Test
+ public void get_content_for_xml_file() {
+ initModel();
+ Reader xmlFileReader = null;
+ try {
+ xmlFileReader = underTest.createReaderForXMLFile("csharp");
+ assertNotNull(xmlFileReader);
+ List<String> lines = IOUtils.readLines(xmlFileReader);
+ assertThat(lines.size()).isEqualTo(25);
+ assertThat(lines.get(0)).isEqualTo("<sqale>");
+ } catch (Exception e) {
+ fail("Should be able to read the XML file.");
+ } finally {
+ IOUtils.closeQuietly(xmlFileReader);
+ }
+ }
+
+ @Test
+ public void return_xml_file_path_for_plugin() {
+ initModel();
+ assertThat(underTest.getXMLFilePath("foo")).isEqualTo(TEST_XML_PREFIX_PATH + "foo-model.xml");
+ }
+
+ @Test
+ public void contain_default_model() {
+ underTest = new DebtModelPluginRepository(mock(PluginRepository.class));
+ underTest.start();
+ assertThat(underTest.getContributingPluginKeyToClassLoader().keySet()).containsOnly("technical-debt");
+ }
+
+ private void initModel() {
+ Map<String, ClassLoader> contributingPluginKeyToClassLoader = Maps.newHashMap();
+ contributingPluginKeyToClassLoader.put("csharp", newClassLoader());
+ contributingPluginKeyToClassLoader.put("java", newClassLoader());
+ underTest = new DebtModelPluginRepository(contributingPluginKeyToClassLoader, TEST_XML_PREFIX_PATH);
+ }
+
+ private ClassLoader newClassLoader() {
+ ClassLoader loader = mock(ClassLoader.class);
+ when(loader.getResourceAsStream(anyString())).thenAnswer(new Answer<InputStream>() {
+ public InputStream answer(InvocationOnMock invocation) throws Throwable {
+ return new FileInputStream(Resources.getResource((String) invocation.getArguments()[0]).getPath());
+ }
+ });
+ return loader;
+ }
+
+ class FakePlugin implements Plugin {
+ @Override public void define(Context context) {
+
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelXMLExporterTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelXMLExporterTest.java
new file mode 100644
index 00000000000..f5e984c9fa3
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelXMLExporterTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.debt;
+
+import com.google.common.io.Resources;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.lang.SystemUtils;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.debt.DebtModelXMLExporter.RuleDebt;
+
+public class DebtModelXMLExporterTest {
+
+ DebtModelXMLExporter underTest = new DebtModelXMLExporter();
+
+ @Test
+ public void export_empty() {
+ assertThat(underTest.export(Collections.emptyList())).isEqualTo("<sqale/>" + SystemUtils.LINE_SEPARATOR);
+ }
+
+ @Test
+ public void export_xml() throws Exception {
+ List<RuleDebt> rules = newArrayList(
+ new RuleDebt().setRuleKey(RuleKey.of("checkstyle", "Regexp"))
+ .setFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name()).setCoefficient("3d").setOffset("15min")
+ );
+
+ assertThat(underTest.export(rules)).isXmlEqualTo(getFileContent("export_xml.xml"));
+ }
+
+ @Test
+ public void pretty_print_exported_xml() {
+ List<RuleDebt> rules = newArrayList(
+ new RuleDebt().setRuleKey(RuleKey.of("checkstyle", "Regexp"))
+ .setFunction(DebtRemediationFunction.Type.LINEAR.name()).setCoefficient("3d")
+ );
+ assertThat(underTest.export(rules)).isEqualTo(
+ "<sqale>" + SystemUtils.LINE_SEPARATOR +
+ " <chc>" + SystemUtils.LINE_SEPARATOR +
+ " <rule-repo>checkstyle</rule-repo>" + SystemUtils.LINE_SEPARATOR +
+ " <rule-key>Regexp</rule-key>" + SystemUtils.LINE_SEPARATOR +
+ " <prop>" + SystemUtils.LINE_SEPARATOR +
+ " <key>remediationFunction</key>" + SystemUtils.LINE_SEPARATOR +
+ " <txt>LINEAR</txt>" + SystemUtils.LINE_SEPARATOR +
+ " </prop>" + SystemUtils.LINE_SEPARATOR +
+ " <prop>" + SystemUtils.LINE_SEPARATOR +
+ " <key>remediationFactor</key>" + SystemUtils.LINE_SEPARATOR +
+ " <val>3</val>" + SystemUtils.LINE_SEPARATOR +
+ " <txt>d</txt>" + SystemUtils.LINE_SEPARATOR +
+ " </prop>" + SystemUtils.LINE_SEPARATOR +
+ " </chc>" + SystemUtils.LINE_SEPARATOR +
+ "</sqale>" + SystemUtils.LINE_SEPARATOR
+ );
+ }
+
+ private static String getFileContent(String file) throws Exception {
+ return Resources.toString(Resources.getResource(DebtModelXMLExporterTest.class, "DebtModelXMLExporterTest/" + file), StandardCharsets.UTF_8);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtRulesXMLImporterTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtRulesXMLImporterTest.java
new file mode 100644
index 00000000000..ea78946bd40
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtRulesXMLImporterTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.debt;
+
+import com.google.common.io.Resources;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.utils.ValidationMessages;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.sonar.server.debt.DebtModelXMLExporter.RuleDebt;
+
+public class DebtRulesXMLImporterTest {
+
+ ValidationMessages validationMessages = ValidationMessages.create();
+ DebtRulesXMLImporter underTest = new DebtRulesXMLImporter();
+
+ @Test
+ public void import_rules() throws Exception {
+ String xml = getFileContent("import_rules.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+
+ assertThat(results).extracting("ruleKey").containsOnly(RuleKey.of("javasquid", "rule1"), RuleKey.of("javasquid", "rule2"));
+ assertThat(validationMessages.getErrors()).isEmpty();
+ assertThat(validationMessages.getWarnings()).isEmpty();
+ }
+
+ @Test
+ public void import_rules_with_deprecated_quality_model_format() throws Exception {
+ String xml = getFileContent("import_rules_with_deprecated_quality_model_format.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+
+ assertThat(results).extracting("ruleKey").containsOnly(RuleKey.of("javasquid", "rule1"), RuleKey.of("javasquid", "rule2"));
+ assertThat(validationMessages.getErrors()).isEmpty();
+ assertThat(validationMessages.getWarnings()).isEmpty();
+ }
+
+ @Test
+ public void import_linear() throws Exception {
+ String xml = getFileContent("import_linear.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp"));
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3h");
+ assertThat(ruleDebt.offset()).isNull();
+ }
+
+ @Test
+ public void import_linear_having_offset_to_zero() throws Exception {
+ String xml = getFileContent("import_linear_having_offset_to_zero.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp"));
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3h");
+ assertThat(ruleDebt.offset()).isNull();
+ }
+
+ @Test
+ public void import_linear_with_offset() throws Exception {
+ String xml = getFileContent("import_linear_with_offset.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3h");
+ assertThat(ruleDebt.offset()).isEqualTo("1min");
+ }
+
+ @Test
+ public void import_constant_issue() throws Exception {
+ String xml = getFileContent("import_constant_issue.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.CONSTANT_ISSUE.name());
+ assertThat(ruleDebt.coefficient()).isNull();
+ assertThat(ruleDebt.offset()).isEqualTo("3d");
+ }
+
+ @Test
+ public void use_default_unit_when_no_unit() throws Exception {
+ String xml = getFileContent("use_default_unit_when_no_unit.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3d");
+ assertThat(ruleDebt.offset()).isEqualTo("1d");
+ }
+
+ @Test
+ public void replace_mn_by_min() throws Exception {
+ String xml = getFileContent("replace_mn_by_min.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3min");
+ assertThat(ruleDebt.offset()).isNull();
+ }
+
+ @Test
+ public void read_integer() throws Exception {
+ String xml = getFileContent("read_integer.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp"));
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3h");
+ assertThat(ruleDebt.offset()).isNull();
+ }
+
+ @Test
+ public void convert_deprecated_linear_with_threshold_function_by_linear_function() throws Exception {
+ String xml = getFileContent("convert_deprecated_linear_with_threshold_function_by_linear_function.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3h");
+ assertThat(ruleDebt.offset()).isNull();
+
+ assertThat(validationMessages.getWarnings()).isNotEmpty();
+ }
+
+ @Test
+ public void convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset() throws Exception {
+ String xml = getFileContent("convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.CONSTANT_ISSUE.name());
+ assertThat(ruleDebt.coefficient()).isNull();
+ assertThat(ruleDebt.offset()).isEqualTo("3h");
+ }
+
+ @Test
+ public void ignore_remediation_cost_having_zero_value() throws Exception {
+ String xml = getFileContent("ignore_remediation_cost_having_zero_value.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void ignore_deprecated_constant_per_file_function() throws Exception {
+ String xml = getFileContent("ignore_deprecated_constant_per_file_function.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).isEmpty();
+
+ assertThat(validationMessages.getWarnings()).isNotEmpty();
+ }
+
+ @Test
+ public void import_badly_formatted_xml() throws Exception {
+ String xml = getFileContent("import_badly_formatted_xml.xml");
+
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).hasSize(1);
+
+ RuleDebt ruleDebt = results.get(0);
+ assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp"));
+ assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name());
+ assertThat(ruleDebt.coefficient()).isEqualTo("3h");
+ assertThat(ruleDebt.offset()).isNull();
+ }
+
+ @Test
+ public void ignore_invalid_value() throws Exception {
+ String xml = getFileContent("ignore_invalid_value.xml");
+ List<RuleDebt> results = underTest.importXML(xml, validationMessages);
+ assertThat(results).isEmpty();
+
+ assertThat(validationMessages.getErrors()).isNotEmpty();
+ }
+
+ @Test
+ public void fail_on_bad_xml() throws Exception {
+ String xml = getFileContent("fail_on_bad_xml.xml");
+
+ try {
+ underTest.importXML(xml, validationMessages);
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(IllegalStateException.class);
+ }
+ }
+
+ private String getFileContent(String file) throws Exception {
+ return Resources.toString(Resources.getResource(getClass(), "DebtRulesXMLImporterTest/" + file),
+ StandardCharsets.UTF_8);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/es/IndexerStartupTaskTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/IndexerStartupTaskTest.java
new file mode 100644
index 00000000000..de10b8e1905
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/IndexerStartupTaskTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.es;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.server.es.metadata.MetadataIndex;
+import org.sonar.server.es.metadata.MetadataIndexImpl;
+import org.sonar.server.es.newindex.FakeIndexDefinition;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.sonar.server.es.newindex.FakeIndexDefinition.TYPE_FAKE;
+
+public class IndexerStartupTaskTest {
+
+ @Rule
+ public EsTester es = EsTester.createCustom(new FakeIndexDefinition());
+
+ private final MapSettings settings = new MapSettings();
+ private final MetadataIndex metadataIndex = mock(MetadataIndexImpl.class);
+ private final StartupIndexer indexer = mock(StartupIndexer.class);
+ private final IndexerStartupTask underTest = new IndexerStartupTask(es.client(), settings.asConfig(), metadataIndex, indexer);
+
+ @Before
+ public void setUp() throws Exception {
+ doReturn(ImmutableSet.of(TYPE_FAKE)).when(indexer).getIndexTypes();
+ }
+
+ @Test
+ public void index_if_not_initialized() {
+ doReturn(false).when(metadataIndex).getInitialized(TYPE_FAKE);
+
+ underTest.execute();
+
+ verify(indexer).getIndexTypes();
+ verify(indexer).indexOnStartup(Mockito.eq(ImmutableSet.of(TYPE_FAKE)));
+ }
+
+ @Test
+ public void set_initialized_after_indexation() {
+ doReturn(false).when(metadataIndex).getInitialized(TYPE_FAKE);
+
+ underTest.execute();
+
+ verify(metadataIndex).setInitialized(eq(TYPE_FAKE), eq(true));
+ }
+
+ @Test
+ public void do_not_index_if_already_initialized() {
+ doReturn(true).when(metadataIndex).getInitialized(TYPE_FAKE);
+
+ underTest.execute();
+
+ verify(indexer).getIndexTypes();
+ verifyNoMoreInteractions(indexer);
+ }
+
+ @Test
+ public void do_not_index_if_indexes_are_disabled() {
+ settings.setProperty("sonar.internal.es.disableIndexes", "true");
+ es.putDocuments(TYPE_FAKE, new FakeDoc());
+
+ underTest.execute();
+
+ // do not index
+ verifyNoMoreInteractions(indexer);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/es/MigrationEsClientImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/MigrationEsClientImplTest.java
new file mode 100644
index 00000000000..4f6633793b6
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/MigrationEsClientImplTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.es;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Iterator;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.elasticsearch.cluster.metadata.MappingMetaData;
+import org.elasticsearch.common.collect.ImmutableOpenMap;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.platform.db.migration.es.MigrationEsClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.es.newindex.SettingsConfiguration.newBuilder;
+
+public class MigrationEsClientImplTest {
+ @Rule
+ public LogTester logTester = new LogTester();
+ @Rule
+ public EsTester es = EsTester.createCustom(
+ new SimpleIndexDefinition("as"),
+ new SimpleIndexDefinition("bs"),
+ new SimpleIndexDefinition("cs"));
+
+ private MigrationEsClient underTest = new MigrationEsClientImpl(es.client());
+
+ @Test
+ public void delete_existing_index() {
+ underTest.deleteIndexes("as");
+
+ assertThat(loadExistingIndices())
+ .toIterable()
+ .doesNotContain("as")
+ .contains("bs", "cs");
+ assertThat(logTester.logs(LoggerLevel.INFO))
+ .contains("Drop Elasticsearch index [as]");
+ }
+
+ @Test
+ public void delete_index_that_does_not_exist() {
+ underTest.deleteIndexes("as", "xxx", "cs");
+
+ assertThat(loadExistingIndices())
+ .toIterable()
+ .doesNotContain("as", "cs")
+ .contains("bs");
+ assertThat(logTester.logs(LoggerLevel.INFO))
+ .contains("Drop Elasticsearch index [as]", "Drop Elasticsearch index [cs]")
+ .doesNotContain("Drop Elasticsearch index [xxx]");
+ }
+
+ @Test
+ public void addMappingToExistingIndex() {
+ Map<String, String> mappingOptions = ImmutableMap.of("norms", "false");
+ underTest.addMappingToExistingIndex("as", "s", "new_field", "keyword", mappingOptions);
+
+ assertThat(loadExistingIndices()).toIterable().contains("as");
+ ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = mappings();
+ MappingMetaData mapping = mappings.get("as").get("s");
+ assertThat(countMappingFields(mapping)).isEqualTo(1);
+ assertThat(field(mapping, "new_field")).isNotNull();
+
+ assertThat(logTester.logs(LoggerLevel.INFO))
+ .contains("Add mapping [new_field] to Elasticsearch index [as]");
+ assertThat(underTest.getUpdatedIndices()).containsExactly("as");
+ }
+
+ private Iterator<String> loadExistingIndices() {
+ return es.client().nativeClient().admin().indices().prepareGetMappings().get().mappings().keysIt();
+ }
+
+ private ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings() {
+ return es.client().nativeClient().admin().indices().prepareGetMappings().get().mappings();
+ }
+
+ @CheckForNull
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> field(MappingMetaData mapping, String field) {
+ Map<String, Object> props = (Map<String, Object>) mapping.getSourceAsMap().get("properties");
+ return (Map<String, Object>) props.get(field);
+ }
+
+ private int countMappingFields(MappingMetaData mapping) {
+ return ((Map) mapping.getSourceAsMap().get("properties")).size();
+ }
+
+ private static class SimpleIndexDefinition implements IndexDefinition {
+ private final String indexName;
+
+ public SimpleIndexDefinition(String indexName) {
+ this.indexName = indexName;
+ }
+
+ @Override
+ public void define(IndexDefinitionContext context) {
+ IndexType.IndexMainType mainType = IndexType.main(Index.simple(indexName), indexName.substring(1));
+ context.create(
+ mainType.getIndex(),
+ newBuilder(new MapSettings().asConfig()).build())
+ .createTypeMapping(mainType);
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java
new file mode 100644
index 00000000000..741d823a6e8
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java
@@ -0,0 +1,1509 @@
+/*
+ * 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.measure.index;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+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.sonar.api.utils.System2;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.organization.OrganizationTesting;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.Facets;
+import org.sonar.server.es.SearchIdResult;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion;
+import org.sonar.server.permission.index.IndexPermissions;
+import org.sonar.server.permission.index.PermissionIndexerTester;
+import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
+import org.sonar.server.tester.UserSessionRule;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Sets.newHashSet;
+import static java.util.Arrays.asList;
+import static java.util.Arrays.stream;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
+import static org.sonar.api.measures.Metric.Level.ERROR;
+import static org.sonar.api.measures.Metric.Level.OK;
+import static org.sonar.api.measures.Metric.Level.WARN;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
+import static org.sonar.db.user.GroupTesting.newGroupDto;
+import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS;
+import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES;
+import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator;
+
+@RunWith(DataProviderRunner.class)
+public class ProjectMeasuresIndexTest {
+
+ private static final String MAINTAINABILITY_RATING = "sqale_rating";
+ private static final String NEW_MAINTAINABILITY_RATING_KEY = "new_maintainability_rating";
+ private static final String RELIABILITY_RATING = "reliability_rating";
+ private static final String NEW_RELIABILITY_RATING = "new_reliability_rating";
+ private static final String SECURITY_RATING = "security_rating";
+ private static final String NEW_SECURITY_RATING = "new_security_rating";
+ private static final String COVERAGE = "coverage";
+ private static final String NEW_COVERAGE = "new_coverage";
+ private static final String DUPLICATION = "duplicated_lines_density";
+ private static final String NEW_DUPLICATION = "new_duplicated_lines_density";
+ private static final String NCLOC = "ncloc";
+ private static final String NEW_LINES = "new_lines";
+ private static final String LANGUAGES = "languages";
+
+ private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto();
+ private static final ComponentDto PROJECT1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-1").setName("Project 1").setDbKey("key-1");
+ private static final ComponentDto PROJECT2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-2").setName("Project 2").setDbKey("key-2");
+ private static final ComponentDto PROJECT3 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-3").setName("Project 3").setDbKey("key-3");
+ private static final UserDto USER1 = newUserDto();
+ private static final UserDto USER2 = newUserDto();
+ private static final GroupDto GROUP1 = newGroupDto();
+ private static final GroupDto GROUP2 = newGroupDto();
+
+ @Rule
+ public EsTester es = EsTester.create();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ @DataProvider
+ public static Object[][] rating_metric_keys() {
+ return new Object[][] {{MAINTAINABILITY_RATING}, {NEW_MAINTAINABILITY_RATING_KEY}, {RELIABILITY_RATING}, {NEW_RELIABILITY_RATING}, {SECURITY_RATING}, {NEW_SECURITY_RATING}};
+ }
+
+ private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client());
+ private PermissionIndexerTester authorizationIndexer = new PermissionIndexerTester(es, projectMeasureIndexer);
+ private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE);
+
+ @Test
+ public void return_empty_if_no_projects() {
+ assertNoResults(new ProjectMeasuresQuery());
+ }
+
+ @Test
+ public void default_sort_is_by_ascending_case_insensitive_name_then_by_key() {
+ ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1");
+ ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2");
+ ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3");
+ ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4");
+ index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2));
+
+ assertResults(new ProjectMeasuresQuery(), apache1, apache2, apachee, windows);
+ }
+
+ @Test
+ public void sort_by_insensitive_name() {
+ ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows");
+ ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee");
+ ComponentDto apache = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache").setName("Apache");
+ index(newDoc(windows), newDoc(apachee), newDoc(apache));
+
+ assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(true), apache, apachee, windows);
+ assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(false), windows, apachee, apache);
+ }
+
+ @Test
+ public void sort_by_ncloc() {
+ index(
+ newDoc(PROJECT1, NCLOC, 15_000d),
+ newDoc(PROJECT2, NCLOC, 30_000d),
+ newDoc(PROJECT3, NCLOC, 1_000d));
+
+ assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), PROJECT3, PROJECT1, PROJECT2);
+ assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), PROJECT2, PROJECT1, PROJECT3);
+ }
+
+ @Test
+ public void sort_by_a_metric_then_by_name_then_by_key() {
+ ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1");
+ ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2");
+ ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3");
+ ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4");
+ index(
+ newDoc(windows, NCLOC, 10_000d),
+ newDoc(apachee, NCLOC, 5_000d),
+ newDoc(apache1, NCLOC, 5_000d),
+ newDoc(apache2, NCLOC, 5_000d));
+
+ assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), apache1, apache2, apachee, windows);
+ assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), windows, apache1, apache2, apachee);
+ }
+
+ @Test
+ public void sort_by_quality_gate_status() {
+ ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4");
+ index(
+ newDoc(PROJECT1).setQualityGateStatus(OK.name()),
+ newDoc(PROJECT2).setQualityGateStatus(ERROR.name()),
+ newDoc(project4).setQualityGateStatus(OK.name()));
+
+ assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), PROJECT1, project4, PROJECT2);
+ assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), PROJECT2, PROJECT1, project4);
+ }
+
+ @Test
+ public void sort_by_quality_gate_status_then_by_name_then_by_key() {
+ ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1");
+ ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2");
+ ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3");
+ ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4");
+ index(
+ newDoc(windows).setQualityGateStatus(ERROR.name()),
+ newDoc(apachee).setQualityGateStatus(OK.name()),
+ newDoc(apache1).setQualityGateStatus(OK.name()),
+ newDoc(apache2).setQualityGateStatus(OK.name()));
+
+ assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), apache1, apache2, apachee, windows);
+ assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), windows, apache1, apache2, apachee);
+ }
+
+ @Test
+ public void paginate_results() {
+ IntStream.rangeClosed(1, 9)
+ .forEach(i -> index(newDoc(newPrivateProjectDto(ORG, "P" + i))));
+
+ SearchIdResult<String> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().setPage(2, 3));
+
+ assertThat(result.getIds()).containsExactly("P4", "P5", "P6");
+ assertThat(result.getTotal()).isEqualTo(9);
+ }
+
+ @Test
+ public void filter_with_lower_than() {
+ index(
+ newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d),
+ newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d),
+ newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 80d));
+
+ assertResults(query, PROJECT1);
+ }
+
+ @Test
+ public void filter_with_lower_than_or_equals() {
+ index(
+ newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d),
+ newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d),
+ newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d));
+
+ assertResults(query, PROJECT1, PROJECT2);
+ }
+
+ @Test
+ public void filter_with_greater_than() {
+ index(
+ newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d),
+ newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d),
+ newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 30_000d));
+ assertResults(query, PROJECT2, PROJECT3);
+
+ query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 100_000d));
+ assertNoResults(query);
+ }
+
+ @Test
+ public void filter_with_greater_than_or_equals() {
+ index(
+ newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d),
+ newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d),
+ newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 30_001d));
+ assertResults(query, PROJECT2, PROJECT3);
+
+ query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 100_000d));
+ assertNoResults(query);
+ }
+
+ @Test
+ public void filter_with_equals() {
+ index(
+ newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d),
+ newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d),
+ newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.EQ, 80d));
+
+ assertResults(query, PROJECT2);
+ }
+
+ @Test
+ public void filter_on_no_data_with_several_projects() {
+ index(
+ newDoc(PROJECT1, NCLOC, 1d),
+ newDoc(PROJECT2, DUPLICATION, 80d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.createNoData(DUPLICATION));
+
+ assertResults(query, PROJECT1);
+ }
+
+ @Test
+ public void filter_on_no_data_should_not_return_projects_with_data_and_other_measures() {
+ ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG);
+ index(newDoc(project, DUPLICATION, 80d, NCLOC, 1d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION));
+
+ assertNoResults(query);
+ }
+
+ @Test
+ public void filter_on_no_data_should_not_return_projects_with_data() {
+ ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG);
+ index(newDoc(project, DUPLICATION, 80d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION));
+
+ assertNoResults(query);
+ }
+
+ @Test
+ public void filter_on_no_data_should_return_projects_with_no_data() {
+ ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG);
+ index(newDoc(project, NCLOC, 1d));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION));
+
+ assertResults(query, project);
+ }
+
+ @Test
+ public void filter_on_several_metrics() {
+ index(
+ newDoc(PROJECT1, COVERAGE, 81d, NCLOC, 10_001d),
+ newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_001d),
+ newDoc(PROJECT3, COVERAGE, 79d, NCLOC, 10_000d));
+
+ ProjectMeasuresQuery esQuery = new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d))
+ .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 10_000d))
+ .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 11_000d));
+ assertResults(esQuery, PROJECT2);
+ }
+
+ @Test
+ public void filter_on_quality_gate_status() {
+ index(
+ newDoc(PROJECT1).setQualityGateStatus(OK.name()),
+ newDoc(PROJECT2).setQualityGateStatus(OK.name()),
+ newDoc(PROJECT3).setQualityGateStatus(ERROR.name()));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery().setQualityGateStatus(OK);
+ assertResults(query, PROJECT1, PROJECT2);
+ }
+
+ @Test
+ public void filter_on_languages() {
+ ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4");
+ index(
+ newDoc(PROJECT1).setLanguages(singletonList("java")),
+ newDoc(PROJECT2).setLanguages(singletonList("xoo")),
+ newDoc(PROJECT3).setLanguages(singletonList("xoo")),
+ newDoc(project4).setLanguages(asList("<null>", "java", "xoo")));
+
+ assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java", "xoo")), PROJECT1, PROJECT2, PROJECT3, project4);
+ assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java")), PROJECT1, project4);
+ assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("unknown")));
+ }
+
+ @Test
+ public void filter_on_query_text() {
+ ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1");
+ ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2");
+ ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3");
+ ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4");
+ index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2));
+
+ assertResults(new ProjectMeasuresQuery().setQueryText("windows"), windows);
+ assertResults(new ProjectMeasuresQuery().setQueryText("project2"), apachee);
+ assertResults(new ProjectMeasuresQuery().setQueryText("pAch"), apache1, apache2, apachee);
+ }
+
+ @Test
+ public void filter_on_ids() {
+ index(
+ newDoc(PROJECT1),
+ newDoc(PROJECT2),
+ newDoc(PROJECT3));
+
+ ProjectMeasuresQuery query = new ProjectMeasuresQuery().setProjectUuids(newHashSet(PROJECT1.uuid(), PROJECT3.uuid()));
+ assertResults(query, PROJECT1, PROJECT3);
+ }
+
+ @Test
+ public void filter_on_tags() {
+ index(
+ newDoc(PROJECT1).setTags(newArrayList("finance", "platform")),
+ newDoc(PROJECT2).setTags(newArrayList("marketing", "platform")),
+ newDoc(PROJECT3).setTags(newArrayList("finance", "language")));
+
+ assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance")), PROJECT1, PROJECT3);
+ assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "language")), PROJECT1, PROJECT3);
+ assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "marketing")), PROJECT1, PROJECT2, PROJECT3);
+ assertResults(new ProjectMeasuresQuery().setTags(newHashSet("marketing")), PROJECT2);
+ assertNoResults(new ProjectMeasuresQuery().setTags(newHashSet("tag 42")));
+ }
+
+ @Test
+ public void filter_on_organization() {
+ OrganizationDto org1 = OrganizationTesting.newOrganizationDto();
+ OrganizationDto org2 = OrganizationTesting.newOrganizationDto();
+ ComponentDto projectInOrg1 = ComponentTesting.newPrivateProjectDto(org1);
+ ComponentDto projectInOrg2 = ComponentTesting.newPrivateProjectDto(org2);
+ index(newDoc(projectInOrg1), newDoc(projectInOrg2));
+
+ ProjectMeasuresQuery query1 = new ProjectMeasuresQuery().setOrganizationUuid(org1.getUuid());
+ assertResults(query1, projectInOrg1);
+
+ ProjectMeasuresQuery query2 = new ProjectMeasuresQuery().setOrganizationUuid(org2.getUuid());
+ assertResults(query2, projectInOrg2);
+
+ ProjectMeasuresQuery query3 = new ProjectMeasuresQuery().setOrganizationUuid("another_org");
+ assertNoResults(query3);
+ }
+
+ @Test
+ public void return_only_projects_authorized_for_user() {
+ indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2));
+ indexForUser(USER2, newDoc(PROJECT3));
+
+ userSession.logIn(USER1);
+ assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2);
+ }
+
+ @Test
+ public void return_only_projects_authorized_for_user_groups() {
+ indexForGroup(GROUP1, newDoc(PROJECT1), newDoc(PROJECT2));
+ indexForGroup(GROUP2, newDoc(PROJECT3));
+
+ userSession.logIn().setGroups(GROUP1);
+ assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2);
+ }
+
+ @Test
+ public void return_only_projects_authorized_for_user_and_groups() {
+ indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2));
+ indexForGroup(GROUP1, newDoc(PROJECT3));
+
+ userSession.logIn(USER1).setGroups(GROUP1);
+ assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2, PROJECT3);
+ }
+
+ @Test
+ public void anonymous_user_can_only_access_projects_authorized_for_anyone() {
+ index(newDoc(PROJECT1));
+ indexForUser(USER1, newDoc(PROJECT2));
+
+ userSession.anonymous();
+ assertResults(new ProjectMeasuresQuery(), PROJECT1);
+ }
+
+ @Test
+ public void root_user_can_access_all_projects() {
+ indexForUser(USER1, newDoc(PROJECT1));
+ // connecting with a root but not USER1
+ userSession.logIn().setRoot();
+
+ assertResults(new ProjectMeasuresQuery(), PROJECT1);
+ }
+
+ @Test
+ public void return_all_projects_when_setIgnoreAuthorization_is_true() {
+ indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2));
+ indexForUser(USER2, newDoc(PROJECT3));
+ userSession.logIn(USER1);
+
+ assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(false), PROJECT1, PROJECT2);
+ assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(true), PROJECT1, PROJECT2, PROJECT3);
+ }
+
+ @Test
+ public void does_not_return_facet_when_no_facets_in_options() {
+ index(
+ newDoc(PROJECT1, NCLOC, 10d, COVERAGE_KEY, 30d, MAINTAINABILITY_RATING, 3d)
+ .setQualityGateStatus(OK.name()));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions()).getFacets();
+
+ assertThat(facets.getAll()).isEmpty();
+ }
+
+ @Test
+ public void facet_ncloc() {
+ index(
+ // 3 docs with ncloc<1K
+ newDoc(NCLOC, 0d),
+ newDoc(NCLOC, 0d),
+ newDoc(NCLOC, 999d),
+ // 2 docs with ncloc>=1K and ncloc<10K
+ newDoc(NCLOC, 1_000d),
+ newDoc(NCLOC, 9_999d),
+ // 4 docs with ncloc>=10K and ncloc<100K
+ newDoc(NCLOC, 10_000d),
+ newDoc(NCLOC, 10_000d),
+ newDoc(NCLOC, 11_000d),
+ newDoc(NCLOC, 99_000d),
+ // 2 docs with ncloc>=100K and ncloc<500K
+ newDoc(NCLOC, 100_000d),
+ newDoc(NCLOC, 499_000d),
+ // 5 docs with ncloc>= 500K
+ newDoc(NCLOC, 500_000d),
+ newDoc(NCLOC, 100_000_000d),
+ newDoc(NCLOC, 500_000d),
+ newDoc(NCLOC, 1_000_000d),
+ newDoc(NCLOC, 100_000_000_000d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets();
+
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 3L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 4L),
+ entry("100000.0-500000.0", 2L),
+ entry("500000.0-*", 5L));
+ }
+
+ @Test
+ public void facet_ncloc_is_sticky() {
+ index(
+ // 1 docs with ncloc<1K
+ newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d),
+ // 2 docs with ncloc>=1K and ncloc<10K
+ newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d),
+ newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d),
+ // 3 docs with ncloc>=10K and ncloc<100K
+ newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d),
+ newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d),
+ newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d),
+ // 2 docs with ncloc>=100K and ncloc<500K
+ newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d),
+ newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 0d),
+ // 1 docs with ncloc>= 500K
+ newDoc(NCLOC, 501_000d, COVERAGE, 81d, DUPLICATION, 20d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 10_000d))
+ .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)),
+ new SearchOptions().addFacets(NCLOC, COVERAGE)).getFacets();
+
+ // Sticky facet on ncloc does not take into account ncloc filter
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 1L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 3L),
+ entry("100000.0-500000.0", 2L),
+ entry("500000.0-*", 0L));
+ // But facet on coverage does well take into into filters
+ assertThat(facets.get(COVERAGE)).containsOnly(
+ entry("NO_DATA", 0L),
+ entry("*-30.0", 3L),
+ entry("30.0-50.0", 0L),
+ entry("50.0-70.0", 0L),
+ entry("70.0-80.0", 0L),
+ entry("80.0-*", 0L));
+ }
+
+ @Test
+ public void facet_ncloc_contains_only_projects_authorized_for_user() {
+ // User can see these projects
+ indexForUser(USER1,
+ // docs with ncloc<1K
+ newDoc(NCLOC, 0d),
+ newDoc(NCLOC, 100d),
+ newDoc(NCLOC, 999d),
+ // docs with ncloc>=1K and ncloc<10K
+ newDoc(NCLOC, 1_000d),
+ newDoc(NCLOC, 9_999d));
+
+ // User cannot see these projects
+ indexForUser(USER2,
+ // doc with ncloc>=10K and ncloc<100K
+ newDoc(NCLOC, 11_000d),
+ // doc with ncloc>=100K and ncloc<500K
+ newDoc(NCLOC, 499_000d),
+ // doc with ncloc>= 500K
+ newDoc(NCLOC, 501_000d));
+
+ userSession.logIn(USER1);
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets();
+
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 3L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 0L),
+ entry("100000.0-500000.0", 0L),
+ entry("500000.0-*", 0L));
+ }
+
+ @Test
+ public void facet_new_lines() {
+ index(
+ // 3 docs with ncloc<1K
+ newDoc(NEW_LINES, 0d),
+ newDoc(NEW_LINES, 0d),
+ newDoc(NEW_LINES, 999d),
+ // 2 docs with ncloc>=1K and ncloc<10K
+ newDoc(NEW_LINES, 1_000d),
+ newDoc(NEW_LINES, 9_999d),
+ // 4 docs with ncloc>=10K and ncloc<100K
+ newDoc(NEW_LINES, 10_000d),
+ newDoc(NEW_LINES, 10_000d),
+ newDoc(NEW_LINES, 11_000d),
+ newDoc(NEW_LINES, 99_000d),
+ // 2 docs with ncloc>=100K and ncloc<500K
+ newDoc(NEW_LINES, 100_000d),
+ newDoc(NEW_LINES, 499_000d),
+ // 5 docs with ncloc>= 500K
+ newDoc(NEW_LINES, 500_000d),
+ newDoc(NEW_LINES, 100_000_000d),
+ newDoc(NEW_LINES, 500_000d),
+ newDoc(NEW_LINES, 1_000_000d),
+ newDoc(NEW_LINES, 100_000_000_000d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_LINES)).getFacets();
+
+ assertThat(facets.get(NEW_LINES)).containsExactly(
+ entry("*-1000.0", 3L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 4L),
+ entry("100000.0-500000.0", 2L),
+ entry("500000.0-*", 5L));
+ }
+
+ @Test
+ public void facet_coverage() {
+ index(
+ // 1 doc with no coverage
+ newDocWithNoMeasure(),
+ // 3 docs with coverage<30%
+ newDoc(COVERAGE, 0d),
+ newDoc(COVERAGE, 0d),
+ newDoc(COVERAGE, 29d),
+ // 2 docs with coverage>=30% and coverage<50%
+ newDoc(COVERAGE, 30d),
+ newDoc(COVERAGE, 49d),
+ // 4 docs with coverage>=50% and coverage<70%
+ newDoc(COVERAGE, 50d),
+ newDoc(COVERAGE, 60d),
+ newDoc(COVERAGE, 60d),
+ newDoc(COVERAGE, 69d),
+ // 2 docs with coverage>=70% and coverage<80%
+ newDoc(COVERAGE, 70d),
+ newDoc(COVERAGE, 79d),
+ // 5 docs with coverage>= 80%
+ newDoc(COVERAGE, 80d),
+ newDoc(COVERAGE, 80d),
+ newDoc(COVERAGE, 90d),
+ newDoc(COVERAGE, 90.5d),
+ newDoc(COVERAGE, 100d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets();
+
+ assertThat(facets.get(COVERAGE)).containsOnly(
+ entry("NO_DATA", 1L),
+ entry("*-30.0", 3L),
+ entry("30.0-50.0", 2L),
+ entry("50.0-70.0", 4L),
+ entry("70.0-80.0", 2L),
+ entry("80.0-*", 5L));
+ }
+
+ @Test
+ public void facet_coverage_is_sticky() {
+ index(
+ // docs with no coverage
+ newDoc(NCLOC, 999d, DUPLICATION, 0d),
+ newDoc(NCLOC, 999d, DUPLICATION, 1d),
+ newDoc(NCLOC, 999d, DUPLICATION, 20d),
+ // docs with coverage<30%
+ newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d),
+ newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d),
+ newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d),
+ // docs with coverage>=30% and coverage<50%
+ newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d),
+ newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d),
+ // docs with coverage>=50% and coverage<70%
+ newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d),
+ // docs with coverage>=70% and coverage<80%
+ newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d),
+ // docs with coverage>= 80%
+ newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 15d),
+ newDoc(NCLOC, 501_000d, COVERAGE, 810d, DUPLICATION, 20d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d))
+ .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)),
+ new SearchOptions().addFacets(COVERAGE, NCLOC)).getFacets();
+
+ // Sticky facet on coverage does not take into account coverage filter
+ assertThat(facets.get(COVERAGE)).containsExactly(
+ entry("NO_DATA", 2L),
+ entry("*-30.0", 3L),
+ entry("30.0-50.0", 2L),
+ entry("50.0-70.0", 1L),
+ entry("70.0-80.0", 1L),
+ entry("80.0-*", 0L));
+ // But facet on ncloc does well take into into filters
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 1L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 0L),
+ entry("100000.0-500000.0", 0L),
+ entry("500000.0-*", 0L));
+ }
+
+ @Test
+ public void facet_coverage_contains_only_projects_authorized_for_user() {
+ // User can see these projects
+ indexForUser(USER1,
+ // 1 doc with no coverage
+ newDocWithNoMeasure(),
+ // docs with coverage<30%
+ newDoc(COVERAGE, 0d),
+ newDoc(COVERAGE, 0d),
+ newDoc(COVERAGE, 29d),
+ // docs with coverage>=30% and coverage<50%
+ newDoc(COVERAGE, 30d),
+ newDoc(COVERAGE, 49d));
+
+ // User cannot see these projects
+ indexForUser(USER2,
+ // 2 docs with no coverage
+ newDocWithNoMeasure(),
+ newDocWithNoMeasure(),
+ // docs with coverage>=50% and coverage<70%
+ newDoc(COVERAGE, 50d),
+ // docs with coverage>=70% and coverage<80%
+ newDoc(COVERAGE, 70d),
+ // docs with coverage>= 80%
+ newDoc(COVERAGE, 80d));
+
+ userSession.logIn(USER1);
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets();
+
+ assertThat(facets.get(COVERAGE)).containsExactly(
+ entry("NO_DATA", 1L),
+ entry("*-30.0", 3L),
+ entry("30.0-50.0", 2L),
+ entry("50.0-70.0", 0L),
+ entry("70.0-80.0", 0L),
+ entry("80.0-*", 0L));
+ }
+
+ @Test
+ public void facet_new_coverage() {
+ index(
+ // 1 doc with no coverage
+ newDocWithNoMeasure(),
+ // 3 docs with coverage<30%
+ newDoc(NEW_COVERAGE, 0d),
+ newDoc(NEW_COVERAGE, 0d),
+ newDoc(NEW_COVERAGE, 29d),
+ // 2 docs with coverage>=30% and coverage<50%
+ newDoc(NEW_COVERAGE, 30d),
+ newDoc(NEW_COVERAGE, 49d),
+ // 4 docs with coverage>=50% and coverage<70%
+ newDoc(NEW_COVERAGE, 50d),
+ newDoc(NEW_COVERAGE, 60d),
+ newDoc(NEW_COVERAGE, 60d),
+ newDoc(NEW_COVERAGE, 69d),
+ // 2 docs with coverage>=70% and coverage<80%
+ newDoc(NEW_COVERAGE, 70d),
+ newDoc(NEW_COVERAGE, 79d),
+ // 5 docs with coverage>= 80%
+ newDoc(NEW_COVERAGE, 80d),
+ newDoc(NEW_COVERAGE, 80d),
+ newDoc(NEW_COVERAGE, 90d),
+ newDoc(NEW_COVERAGE, 90.5d),
+ newDoc(NEW_COVERAGE, 100d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_COVERAGE)).getFacets();
+
+ assertThat(facets.get(NEW_COVERAGE)).containsOnly(
+ entry("NO_DATA", 1L),
+ entry("*-30.0", 3L),
+ entry("30.0-50.0", 2L),
+ entry("50.0-70.0", 4L),
+ entry("70.0-80.0", 2L),
+ entry("80.0-*", 5L));
+ }
+
+ @Test
+ public void facet_duplicated_lines_density() {
+ index(
+ // 1 doc with no duplication
+ newDocWithNoMeasure(),
+ // 3 docs with duplication<3%
+ newDoc(DUPLICATION, 0d),
+ newDoc(DUPLICATION, 0d),
+ newDoc(DUPLICATION, 2.9d),
+ // 2 docs with duplication>=3% and duplication<5%
+ newDoc(DUPLICATION, 3d),
+ newDoc(DUPLICATION, 4.9d),
+ // 4 docs with duplication>=5% and duplication<10%
+ newDoc(DUPLICATION, 5d),
+ newDoc(DUPLICATION, 6d),
+ newDoc(DUPLICATION, 6d),
+ newDoc(DUPLICATION, 9.9d),
+ // 2 docs with duplication>=10% and duplication<20%
+ newDoc(DUPLICATION, 10d),
+ newDoc(DUPLICATION, 19.9d),
+ // 5 docs with duplication>= 20%
+ newDoc(DUPLICATION, 20d),
+ newDoc(DUPLICATION, 20d),
+ newDoc(DUPLICATION, 50d),
+ newDoc(DUPLICATION, 80d),
+ newDoc(DUPLICATION, 100d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets();
+
+ assertThat(facets.get(DUPLICATION)).containsOnly(
+ entry("NO_DATA", 1L),
+ entry("*-3.0", 3L),
+ entry("3.0-5.0", 2L),
+ entry("5.0-10.0", 4L),
+ entry("10.0-20.0", 2L),
+ entry("20.0-*", 5L));
+ }
+
+ @Test
+ public void facet_duplicated_lines_density_is_sticky() {
+ index(
+ // docs with no duplication
+ newDoc(NCLOC, 50_001d, COVERAGE, 29d),
+ // docs with duplication<3%
+ newDoc(DUPLICATION, 0d, NCLOC, 999d, COVERAGE, 0d),
+ // docs with duplication>=3% and duplication<5%
+ newDoc(DUPLICATION, 3d, NCLOC, 5000d, COVERAGE, 0d),
+ newDoc(DUPLICATION, 4.9d, NCLOC, 6000d, COVERAGE, 0d),
+ // docs with duplication>=5% and duplication<10%
+ newDoc(DUPLICATION, 5d, NCLOC, 11000d, COVERAGE, 0d),
+ // docs with duplication>=10% and duplication<20%
+ newDoc(DUPLICATION, 10d, NCLOC, 120000d, COVERAGE, 10d),
+ newDoc(DUPLICATION, 19.9d, NCLOC, 130000d, COVERAGE, 20d),
+ // docs with duplication>= 20%
+ newDoc(DUPLICATION, 20d, NCLOC, 1000000d, COVERAGE, 40d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d))
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)),
+ new SearchOptions().addFacets(DUPLICATION, NCLOC)).getFacets();
+
+ // Sticky facet on duplication does not take into account duplication filter
+ assertThat(facets.get(DUPLICATION)).containsOnly(
+ entry("NO_DATA", 1L),
+ entry("*-3.0", 1L),
+ entry("3.0-5.0", 2L),
+ entry("5.0-10.0", 1L),
+ entry("10.0-20.0", 2L),
+ entry("20.0-*", 0L));
+ // But facet on ncloc does well take into into filters
+ assertThat(facets.get(NCLOC)).containsOnly(
+ entry("*-1000.0", 1L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 1L),
+ entry("100000.0-500000.0", 0L),
+ entry("500000.0-*", 0L));
+ }
+
+ @Test
+ public void facet_duplicated_lines_density_contains_only_projects_authorized_for_user() {
+ // User can see these projects
+ indexForUser(USER1,
+ // docs with no duplication
+ newDocWithNoMeasure(),
+ // docs with duplication<3%
+ newDoc(DUPLICATION, 0d),
+ newDoc(DUPLICATION, 0d),
+ newDoc(DUPLICATION, 2.9d),
+ // docs with duplication>=3% and duplication<5%
+ newDoc(DUPLICATION, 3d),
+ newDoc(DUPLICATION, 4.9d));
+
+ // User cannot see these projects
+ indexForUser(USER2,
+ // docs with no duplication
+ newDocWithNoMeasure(),
+ newDocWithNoMeasure(),
+ // docs with duplication>=5% and duplication<10%
+ newDoc(DUPLICATION, 5d),
+ // docs with duplication>=10% and duplication<20%
+ newDoc(DUPLICATION, 10d),
+ // docs with duplication>= 20%
+ newDoc(DUPLICATION, 20d));
+
+ userSession.logIn(USER1);
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets();
+
+ assertThat(facets.get(DUPLICATION)).containsOnly(
+ entry("NO_DATA", 1L),
+ entry("*-3.0", 3L),
+ entry("3.0-5.0", 2L),
+ entry("5.0-10.0", 0L),
+ entry("10.0-20.0", 0L),
+ entry("20.0-*", 0L));
+ }
+
+ @Test
+ public void facet_new_duplicated_lines_density() {
+ index(
+ // 2 docs with no measure
+ newDocWithNoMeasure(),
+ newDocWithNoMeasure(),
+ // 3 docs with duplication<3%
+ newDoc(NEW_DUPLICATION, 0d),
+ newDoc(NEW_DUPLICATION, 0d),
+ newDoc(NEW_DUPLICATION, 2.9d),
+ // 2 docs with duplication>=3% and duplication<5%
+ newDoc(NEW_DUPLICATION, 3d),
+ newDoc(NEW_DUPLICATION, 4.9d),
+ // 4 docs with duplication>=5% and duplication<10%
+ newDoc(NEW_DUPLICATION, 5d),
+ newDoc(NEW_DUPLICATION, 6d),
+ newDoc(NEW_DUPLICATION, 6d),
+ newDoc(NEW_DUPLICATION, 9.9d),
+ // 2 docs with duplication>=10% and duplication<20%
+ newDoc(NEW_DUPLICATION, 10d),
+ newDoc(NEW_DUPLICATION, 19.9d),
+ // 5 docs with duplication>= 20%
+ newDoc(NEW_DUPLICATION, 20d),
+ newDoc(NEW_DUPLICATION, 20d),
+ newDoc(NEW_DUPLICATION, 50d),
+ newDoc(NEW_DUPLICATION, 80d),
+ newDoc(NEW_DUPLICATION, 100d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_DUPLICATION)).getFacets();
+
+ assertThat(facets.get(NEW_DUPLICATION)).containsExactly(
+ entry("NO_DATA", 2L),
+ entry("*-3.0", 3L),
+ entry("3.0-5.0", 2L),
+ entry("5.0-10.0", 4L),
+ entry("10.0-20.0", 2L),
+ entry("20.0-*", 5L));
+ }
+
+ @Test
+ @UseDataProvider("rating_metric_keys")
+ public void facet_on_rating(String metricKey) {
+ index(
+ // 3 docs with rating A
+ newDoc(metricKey, 1d),
+ newDoc(metricKey, 1d),
+ newDoc(metricKey, 1d),
+ // 2 docs with rating B
+ newDoc(metricKey, 2d),
+ newDoc(metricKey, 2d),
+ // 4 docs with rating C
+ newDoc(metricKey, 3d),
+ newDoc(metricKey, 3d),
+ newDoc(metricKey, 3d),
+ newDoc(metricKey, 3d),
+ // 2 docs with rating D
+ newDoc(metricKey, 4d),
+ newDoc(metricKey, 4d),
+ // 5 docs with rating E
+ newDoc(metricKey, 5d),
+ newDoc(metricKey, 5d),
+ newDoc(metricKey, 5d),
+ newDoc(metricKey, 5d),
+ newDoc(metricKey, 5d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets();
+
+ assertThat(facets.get(metricKey)).containsExactly(
+ entry("1", 3L),
+ entry("2", 2L),
+ entry("3", 4L),
+ entry("4", 2L),
+ entry("5", 5L));
+ }
+
+ @Test
+ @UseDataProvider("rating_metric_keys")
+ public void facet_on_rating_is_sticky(String metricKey) {
+ index(
+ // docs with rating A
+ newDoc(metricKey, 1d, NCLOC, 100d, COVERAGE, 0d),
+ newDoc(metricKey, 1d, NCLOC, 200d, COVERAGE, 0d),
+ newDoc(metricKey, 1d, NCLOC, 999d, COVERAGE, 0d),
+ // docs with rating B
+ newDoc(metricKey, 2d, NCLOC, 2000d, COVERAGE, 0d),
+ newDoc(metricKey, 2d, NCLOC, 5000d, COVERAGE, 0d),
+ // docs with rating C
+ newDoc(metricKey, 3d, NCLOC, 20000d, COVERAGE, 0d),
+ newDoc(metricKey, 3d, NCLOC, 30000d, COVERAGE, 0d),
+ newDoc(metricKey, 3d, NCLOC, 40000d, COVERAGE, 0d),
+ newDoc(metricKey, 3d, NCLOC, 50000d, COVERAGE, 0d),
+ // docs with rating D
+ newDoc(metricKey, 4d, NCLOC, 120000d, COVERAGE, 0d),
+ // docs with rating E
+ newDoc(metricKey, 5d, NCLOC, 600000d, COVERAGE, 40d),
+ newDoc(metricKey, 5d, NCLOC, 700000d, COVERAGE, 50d),
+ newDoc(metricKey, 5d, NCLOC, 800000d, COVERAGE, 60d));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery()
+ .addMetricCriterion(MetricCriterion.create(metricKey, Operator.LT, 3d))
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)),
+ new SearchOptions().addFacets(metricKey, NCLOC)).getFacets();
+
+ // Sticky facet on maintainability rating does not take into account maintainability rating filter
+ assertThat(facets.get(metricKey)).containsExactly(
+ entry("1", 3L),
+ entry("2", 2L),
+ entry("3", 4L),
+ entry("4", 1L),
+ entry("5", 0L));
+ // But facet on ncloc does well take into into filters
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 3L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 0L),
+ entry("100000.0-500000.0", 0L),
+ entry("500000.0-*", 0L));
+ }
+
+ @Test
+ @UseDataProvider("rating_metric_keys")
+ public void facet_on_rating_contains_only_projects_authorized_for_user(String metricKey) {
+ // User can see these projects
+ indexForUser(USER1,
+ // 3 docs with rating A
+ newDoc(metricKey, 1d),
+ newDoc(metricKey, 1d),
+ newDoc(metricKey, 1d),
+ // 2 docs with rating B
+ newDoc(metricKey, 2d),
+ newDoc(metricKey, 2d));
+
+ // User cannot see these projects
+ indexForUser(USER2,
+ // docs with rating C
+ newDoc(metricKey, 3d),
+ // docs with rating D
+ newDoc(metricKey, 4d),
+ // docs with rating E
+ newDoc(metricKey, 5d));
+
+ userSession.logIn(USER1);
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets();
+
+ assertThat(facets.get(metricKey)).containsExactly(
+ entry("1", 3L),
+ entry("2", 2L),
+ entry("3", 0L),
+ entry("4", 0L),
+ entry("5", 0L));
+ }
+
+ @Test
+ public void facet_quality_gate() {
+ index(
+ // 2 docs with QG OK
+ newDoc().setQualityGateStatus(OK.name()),
+ newDoc().setQualityGateStatus(OK.name()),
+ // 4 docs with QG ERROR
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()));
+
+ LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY);
+
+ assertThat(result).containsOnly(
+ entry(ERROR.name(), 4L),
+ entry(OK.name(), 2L),
+ entry(WARN.name(), 0L));
+ }
+
+ @Test
+ public void facet_quality_gate_is_sticky() {
+ index(
+ // 2 docs with QG OK
+ newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()),
+ newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()),
+ // 4 docs with QG ERROR
+ newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(ERROR.name()),
+ newDoc(NCLOC, 5000d, COVERAGE, 40d).setQualityGateStatus(ERROR.name()),
+ newDoc(NCLOC, 12000d, COVERAGE, 50d).setQualityGateStatus(ERROR.name()),
+ newDoc(NCLOC, 13000d, COVERAGE, 60d).setQualityGateStatus(ERROR.name()));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery()
+ .setQualityGateStatus(ERROR)
+ .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 55d)),
+ new SearchOptions().addFacets(ALERT_STATUS_KEY, NCLOC)).getFacets();
+
+ // Sticky facet on quality gate does not take into account quality gate filter
+ assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly(
+ entry(OK.name(), 2L),
+ entry(ERROR.name(), 3L),
+ entry(WARN.name(), 0L));
+ // But facet on ncloc does well take into into filters
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 1L),
+ entry("1000.0-10000.0", 1L),
+ entry("10000.0-100000.0", 1L),
+ entry("100000.0-500000.0", 0L),
+ entry("500000.0-*", 0L));
+ }
+
+ @Test
+ public void facet_quality_gate_contains_only_projects_authorized_for_user() {
+ // User can see these projects
+ indexForUser(USER1,
+ // 2 docs with QG OK
+ newDoc().setQualityGateStatus(OK.name()),
+ newDoc().setQualityGateStatus(OK.name()));
+
+ // User cannot see these projects
+ indexForUser(USER2,
+ // 4 docs with QG ERROR
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()));
+
+ userSession.logIn(USER1);
+ LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY);
+
+ assertThat(result).containsOnly(
+ entry(ERROR.name(), 0L),
+ entry(OK.name(), 2L),
+ entry(WARN.name(), 0L));
+ }
+
+ @Test
+ public void facet_quality_gate_using_deprecated_warning() {
+ index(
+ // 2 docs with QG OK
+ newDoc().setQualityGateStatus(OK.name()),
+ newDoc().setQualityGateStatus(OK.name()),
+ // 3 docs with QG WARN
+ newDoc().setQualityGateStatus(WARN.name()),
+ newDoc().setQualityGateStatus(WARN.name()),
+ newDoc().setQualityGateStatus(WARN.name()),
+ // 4 docs with QG ERROR
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()));
+
+ LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY);
+
+ assertThat(result).containsOnly(
+ entry(ERROR.name(), 4L),
+ entry(WARN.name(), 3L),
+ entry(OK.name(), 2L));
+ }
+
+ @Test
+ public void facet_quality_gate_does_not_return_deprecated_warning_when_set_ignore_warning_is_true() {
+ index(
+ // 2 docs with QG OK
+ newDoc().setQualityGateStatus(OK.name()),
+ newDoc().setQualityGateStatus(OK.name()),
+ // 4 docs with QG ERROR
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()),
+ newDoc().setQualityGateStatus(ERROR.name()));
+
+ assertThat(underTest.search(new ProjectMeasuresQuery().setIgnoreWarning(true), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY)).containsOnly(
+ entry(ERROR.name(), 4L),
+ entry(OK.name(), 2L));
+ assertThat(underTest.search(new ProjectMeasuresQuery().setIgnoreWarning(false), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY)).containsOnly(
+ entry(ERROR.name(), 4L),
+ entry(WARN.name(), 0L),
+ entry(OK.name(), 2L));
+ }
+
+ @Test
+ public void facet_languages() {
+ index(
+ newDoc().setLanguages(singletonList("java")),
+ newDoc().setLanguages(singletonList("java")),
+ newDoc().setLanguages(singletonList("xoo")),
+ newDoc().setLanguages(singletonList("xml")),
+ newDoc().setLanguages(asList("<null>", "java")),
+ newDoc().setLanguages(asList("<null>", "java", "xoo")));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets();
+
+ assertThat(facets.get(LANGUAGES)).containsOnly(
+ entry("<null>", 2L),
+ entry("java", 4L),
+ entry("xoo", 2L),
+ entry("xml", 1L));
+ }
+
+ @Test
+ public void facet_languages_is_limited_to_10_languages() {
+ index(
+ newDoc().setLanguages(asList("<null>", "java", "xoo", "css", "cpp")),
+ newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")),
+ newDoc().setLanguages(asList("js", "scala")));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets();
+
+ assertThat(facets.get(LANGUAGES)).hasSize(10);
+ }
+
+ @Test
+ public void facet_languages_is_sticky() {
+ index(
+ newDoc(NCLOC, 10d).setLanguages(singletonList("java")),
+ newDoc(NCLOC, 10d).setLanguages(singletonList("java")),
+ newDoc(NCLOC, 10d).setLanguages(singletonList("xoo")),
+ newDoc(NCLOC, 100d).setLanguages(singletonList("xml")),
+ newDoc(NCLOC, 100d).setLanguages(asList("<null>", "java")),
+ newDoc(NCLOC, 5000d).setLanguages(asList("<null>", "java", "xoo")));
+
+ Facets facets = underTest.search(
+ new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("java")),
+ new SearchOptions().addFacets(LANGUAGES, NCLOC)).getFacets();
+
+ // Sticky facet on language does not take into account language filter
+ assertThat(facets.get(LANGUAGES)).containsOnly(
+ entry("<null>", 2L),
+ entry("java", 4L),
+ entry("xoo", 2L),
+ entry("xml", 1L));
+ // But facet on ncloc does well take account into filters
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 3L),
+ entry("1000.0-10000.0", 1L),
+ entry("10000.0-100000.0", 0L),
+ entry("100000.0-500000.0", 0L),
+ entry("500000.0-*", 0L));
+ }
+
+ @Test
+ public void facet_languages_returns_more_than_10_languages_when_languages_filter_contains_value_not_in_top_10() {
+ index(
+ newDoc().setLanguages(asList("<null>", "java", "xoo", "css", "cpp")),
+ newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")),
+ newDoc().setLanguages(asList("js", "scala")));
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("xoo", "xml")), new SearchOptions().addFacets(LANGUAGES)).getFacets();
+
+ assertThat(facets.get(LANGUAGES)).containsOnly(
+ entry("<null>", 1L),
+ entry("cpp", 1L),
+ entry("css", 1L),
+ entry("java", 1L),
+ entry("js", 1L),
+ entry("perl", 1L),
+ entry("php", 1L),
+ entry("python", 1L),
+ entry("ruby", 1L),
+ entry("scala", 1L),
+ entry("xoo", 1L),
+ entry("xml", 1L));
+ }
+
+ @Test
+ public void facet_languages_contains_only_projects_authorized_for_user() {
+ // User can see these projects
+ indexForUser(USER1,
+ newDoc().setLanguages(singletonList("java")),
+ newDoc().setLanguages(asList("java", "xoo")));
+
+ // User cannot see these projects
+ indexForUser(USER2,
+ newDoc().setLanguages(singletonList("java")),
+ newDoc().setLanguages(asList("java", "xoo")));
+
+ userSession.logIn(USER1);
+ LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets().get(LANGUAGES);
+
+ assertThat(result).containsOnly(
+ entry("java", 2L),
+ entry("xoo", 1L));
+ }
+
+ @Test
+ public void facet_tags() {
+ index(
+ newDoc().setTags(newArrayList("finance", "offshore", "java")),
+ newDoc().setTags(newArrayList("finance", "javascript")),
+ newDoc().setTags(newArrayList("marketing", "finance")),
+ newDoc().setTags(newArrayList("marketing", "offshore")),
+ newDoc().setTags(newArrayList("finance", "marketing")),
+ newDoc().setTags(newArrayList("finance")));
+
+ Map<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS);
+
+ assertThat(result).containsOnly(
+ entry("finance", 5L),
+ entry("marketing", 3L),
+ entry("offshore", 2L),
+ entry("java", 1L),
+ entry("javascript", 1L));
+ }
+
+ @Test
+ public void facet_tags_is_sticky() {
+ index(
+ newDoc().setTags(newArrayList("finance")).setQualityGateStatus(OK.name()),
+ newDoc().setTags(newArrayList("finance")).setQualityGateStatus(ERROR.name()),
+ newDoc().setTags(newArrayList("cpp")).setQualityGateStatus(ERROR.name()));
+
+ Facets facets = underTest.search(
+ new ProjectMeasuresQuery().setTags(newHashSet("cpp")),
+ new SearchOptions().addFacets(FIELD_TAGS).addFacets(ALERT_STATUS_KEY))
+ .getFacets();
+
+ assertThat(facets.get(FIELD_TAGS)).containsOnly(
+ entry("finance", 2L),
+ entry("cpp", 1L));
+ assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly(
+ entry(OK.name(), 0L),
+ entry(ERROR.name(), 1L),
+ entry(WARN.name(), 0L));
+ }
+
+ @Test
+ public void facet_tags_returns_10_elements_by_default() {
+ index(
+ newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")),
+ newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")),
+ newDoc().setTags(newArrayList("solo")));
+
+ Map<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS);
+
+ assertThat(result).hasSize(10).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10");
+ }
+
+ @Test
+ public void facet_tags_returns_more_than_10_tags_when_tags_filter_contains_value_not_in_top_10() {
+ index(
+ newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")),
+ newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")),
+ newDoc().setTags(newArrayList("solo", "solo2")));
+
+ Map<String, Long> result = underTest.search(new ProjectMeasuresQuery().setTags(ImmutableSet.of("solo", "solo2")), new SearchOptions().addFacets(FIELD_TAGS)).getFacets()
+ .get(FIELD_TAGS);
+
+ assertThat(result).hasSize(12).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10", "solo",
+ "solo2");
+ }
+
+ @Test
+ public void search_tags() {
+ index(
+ newDoc().setTags(newArrayList("finance", "offshore", "java")),
+ newDoc().setTags(newArrayList("official", "javascript")),
+ newDoc().setTags(newArrayList("marketing", "official")),
+ newDoc().setTags(newArrayList("marketing", "Madhoff")),
+ newDoc().setTags(newArrayList("finance", "offshore")),
+ newDoc().setTags(newArrayList("offshore")));
+
+ List<String> result = underTest.searchTags("off", 10);
+
+ assertThat(result).containsOnly("offshore", "official", "Madhoff");
+ }
+
+ @Test
+ public void search_tags_return_all_tags() {
+ index(
+ newDoc().setTags(newArrayList("finance", "offshore", "java")),
+ newDoc().setTags(newArrayList("official", "javascript")),
+ newDoc().setTags(newArrayList("marketing", "official")),
+ newDoc().setTags(newArrayList("marketing", "Madhoff")),
+ newDoc().setTags(newArrayList("finance", "offshore")),
+ newDoc().setTags(newArrayList("offshore")));
+
+ List<String> result = underTest.searchTags(null, 10);
+
+ assertThat(result).containsOnly("offshore", "official", "Madhoff", "finance", "marketing", "java", "javascript");
+ }
+
+ @Test
+ public void search_tags_in_lexical_order() {
+ index(
+ newDoc().setTags(newArrayList("finance", "offshore", "java")),
+ newDoc().setTags(newArrayList("official", "javascript")),
+ newDoc().setTags(newArrayList("marketing", "official")),
+ newDoc().setTags(newArrayList("marketing", "Madhoff")),
+ newDoc().setTags(newArrayList("finance", "offshore")),
+ newDoc().setTags(newArrayList("offshore")));
+
+ List<String> result = underTest.searchTags(null, 10);
+
+ assertThat(result).containsExactly("Madhoff", "finance", "java", "javascript", "marketing", "official", "offshore");
+ }
+
+ @Test
+ public void search_tags_only_of_authorized_projects() {
+ indexForUser(USER1,
+ newDoc(PROJECT1).setTags(singletonList("finance")),
+ newDoc(PROJECT2).setTags(singletonList("marketing")));
+ indexForUser(USER2,
+ newDoc(PROJECT3).setTags(singletonList("offshore")));
+
+ userSession.logIn(USER1);
+
+ List<String> result = underTest.searchTags(null, 10);
+
+ assertThat(result).containsOnly("finance", "marketing");
+ }
+
+ @Test
+ public void search_tags_with_no_tags() {
+ List<String> result = underTest.searchTags("whatever", 10);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void search_tags_with_page_size_at_0() {
+ index(newDoc().setTags(newArrayList("offshore")));
+
+ List<String> result = underTest.searchTags(null, 0);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void search_statistics() {
+ es.putDocuments(TYPE_PROJECT_MEASURES,
+ newDoc("lines", 10, "coverage", 80)
+ .setLanguages(Arrays.asList("java", "cs", "js"))
+ .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 200, "cs", 250, "js", 50)),
+ newDoc("lines", 20, "coverage", 80)
+ .setLanguages(Arrays.asList("java", "python", "kotlin"))
+ .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 300, "python", 100, "kotlin", 404)));
+
+ ProjectMeasuresStatistics result = underTest.searchTelemetryStatistics();
+
+ assertThat(result.getProjectCount()).isEqualTo(2);
+ assertThat(result.getProjectCountByLanguage()).containsOnly(
+ entry("java", 2L), entry("cs", 1L), entry("js", 1L), entry("python", 1L), entry("kotlin", 1L));
+ assertThat(result.getNclocByLanguage()).containsOnly(
+ entry("java", 500L), entry("cs", 250L), entry("js", 50L), entry("python", 100L), entry("kotlin", 404L));
+ }
+
+ @Test
+ public void fail_if_page_size_greater_than_500() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Page size must be lower than or equals to 500");
+
+ underTest.searchTags("whatever", 501);
+ }
+
+ private void index(ProjectMeasuresDoc... docs) {
+ es.putDocuments(TYPE_PROJECT_MEASURES, docs);
+ authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).allowAnyone()).collect(toList()));
+ }
+
+ private void indexForUser(UserDto user, ProjectMeasuresDoc... docs) {
+ es.putDocuments(TYPE_PROJECT_MEASURES, docs);
+ authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).addUserId(user.getId())).collect(toList()));
+ }
+
+ private void indexForGroup(GroupDto group, ProjectMeasuresDoc... docs) {
+ es.putDocuments(TYPE_PROJECT_MEASURES, docs);
+ authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).addGroupId(group.getId())).collect(toList()));
+ }
+
+ private static ProjectMeasuresDoc newDoc(ComponentDto project) {
+ return new ProjectMeasuresDoc()
+ .setOrganizationUuid(project.getOrganizationUuid())
+ .setId(project.uuid())
+ .setKey(project.getDbKey())
+ .setName(project.name());
+ }
+
+ private static ProjectMeasuresDoc newDoc() {
+ return newDoc(ComponentTesting.newPrivateProjectDto(ORG));
+ }
+
+ private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) {
+ return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1)));
+ }
+
+ private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2) {
+ return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2)));
+ }
+
+ private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) {
+ return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2), newMeasure(metric3, value3)));
+ }
+
+ private static Map<String, Object> newMeasure(String key, Object value) {
+ return ImmutableMap.of("key", key, "value", value);
+ }
+
+ private static ProjectMeasuresDoc newDocWithNoMeasure() {
+ return newDoc(ComponentTesting.newPrivateProjectDto(ORG));
+ }
+
+ private static ProjectMeasuresDoc newDoc(String metric1, Object value1) {
+ return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1);
+ }
+
+ private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2) {
+ return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2);
+ }
+
+ private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) {
+ return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2, metric3, value3);
+ }
+
+ private void assertResults(ProjectMeasuresQuery query, ComponentDto... expectedProjects) {
+ List<String> result = underTest.search(query, new SearchOptions()).getIds();
+ assertThat(result).containsExactly(Arrays.stream(expectedProjects).map(ComponentDto::uuid).toArray(String[]::new));
+ }
+
+ private void assertNoResults(ProjectMeasuresQuery query) {
+ assertResults(query);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java
new file mode 100644
index 00000000000..344b598c5c7
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java
@@ -0,0 +1,333 @@
+/*
+ * 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.measure.index;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.organization.OrganizationTesting;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.Facets;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion;
+import org.sonar.server.permission.index.IndexPermissions;
+import org.sonar.server.permission.index.PermissionIndexerTester;
+import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
+import org.sonar.server.tester.UserSessionRule;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
+import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES;
+import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.GT;
+import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.LT;
+
+public class ProjectMeasuresIndexTextSearchTest {
+
+ private static final String NCLOC = "ncloc";
+
+ private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto();
+
+ @Rule
+ public EsTester es = EsTester.create();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client());
+ private PermissionIndexerTester authorizationIndexer = new PermissionIndexerTester(es, projectMeasureIndexer);
+ private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE);
+
+ @Test
+ public void match_exact_case_insensitive_name() {
+ index(
+ newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")),
+ newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube")));
+
+ assertTextQueryResults("Apache Struts", "struts");
+ assertTextQueryResults("APACHE STRUTS", "struts");
+ assertTextQueryResults("APACHE struTS", "struts");
+ }
+
+ @Test
+ public void match_from_sub_name() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")));
+
+ assertTextQueryResults("truts", "struts");
+ assertTextQueryResults("pache", "struts");
+ assertTextQueryResults("apach", "struts");
+ assertTextQueryResults("che stru", "struts");
+ }
+
+ @Test
+ public void match_name_with_dot() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache.Struts")));
+
+ assertTextQueryResults("apache struts", "struts");
+ }
+
+ @Test
+ public void match_partial_name() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("XstrutsxXjavax")));
+
+ assertTextQueryResults("struts java", "struts");
+ }
+
+ @Test
+ public void match_partial_name_prefix_word1() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.java")));
+
+ assertTextQueryResults("struts java", "struts");
+ }
+
+ @Test
+ public void match_partial_name_suffix_word1() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("StrutsObject.java")));
+
+ assertTextQueryResults("struts java", "struts");
+ }
+
+ @Test
+ public void match_partial_name_prefix_word2() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.xjava")));
+
+ assertTextQueryResults("struts java", "struts");
+ }
+
+ @Test
+ public void match_partial_name_suffix_word2() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStrutsObject.xjavax")));
+
+ assertTextQueryResults("struts java", "struts");
+ }
+
+ @Test
+ public void match_subset_of_document_terms() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Some.Struts.Project.java.old")));
+
+ assertTextQueryResults("struts java", "struts");
+ }
+
+ @Test
+ public void match_partial_match_prefix_and_suffix_everywhere() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.javax")));
+
+ assertTextQueryResults("struts java", "struts");
+ }
+
+ @Test
+ public void ignore_empty_words() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Struts")));
+
+ assertTextQueryResults(" struts \n \n\n", "struts");
+ }
+
+ @Test
+ public void match_name_from_prefix() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")));
+
+ assertTextQueryResults("apach", "struts");
+ assertTextQueryResults("ApA", "struts");
+ assertTextQueryResults("AP", "struts");
+ }
+
+ @Test
+ public void match_name_from_two_words() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("ApacheStrutsFoundation")));
+
+ assertTextQueryResults("apache struts", "project");
+ assertTextQueryResults("struts apache", "project");
+ // Only one word is matching
+ assertNoResults("apache plugin");
+ assertNoResults("project struts");
+ }
+
+ @Test
+ public void match_long_name() {
+ index(
+ newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("LongNameLongNameLongNameLongNameSonarQube")),
+ newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("LongNameLongNameLongNameLongNameSonarQubeX")));
+
+ assertTextQueryResults("LongNameLongNameLongNameLongNameSonarQube", "project1", "project2");
+ }
+
+ @Test
+ public void match_name_with_two_characters() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")));
+
+ assertTextQueryResults("st", "struts");
+ assertTextQueryResults("tr", "struts");
+ }
+
+ @Test
+ public void match_exact_case_insensitive_key() {
+ index(
+ newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1")),
+ newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2")));
+
+ assertTextQueryResults("project1", "project1");
+ assertTextQueryResults("PROJECT1", "project1");
+ assertTextQueryResults("pRoJecT1", "project1");
+ }
+
+ @Test
+ public void match_key_with_dot() {
+ index(
+ newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube")),
+ newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube")));
+
+ assertTextQueryResults("org.sonarqube", "sonarqube");
+ assertNoResults("orgsonarqube");
+ assertNoResults("org-sonarqube");
+ assertNoResults("org:sonarqube");
+ assertNoResults("org sonarqube");
+ }
+
+ @Test
+ public void match_key_with_dash() {
+ index(
+ newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org-sonarqube")),
+ newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube")));
+
+ assertTextQueryResults("org-sonarqube", "sonarqube");
+ assertNoResults("orgsonarqube");
+ assertNoResults("org.sonarqube");
+ assertNoResults("org:sonarqube");
+ assertNoResults("org sonarqube");
+ }
+
+ @Test
+ public void match_key_with_colon() {
+ index(
+ newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org:sonarqube")),
+ newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube")));
+
+ assertTextQueryResults("org:sonarqube", "sonarqube");
+ assertNoResults("orgsonarqube");
+ assertNoResults("org-sonarqube");
+ assertNoResults("org_sonarqube");
+ assertNoResults("org sonarqube");
+ }
+
+ @Test
+ public void match_key_having_all_special_characters() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube:sonar-sérvèr_ç")));
+
+ assertTextQueryResults("org.sonarqube:sonar-sérvèr_ç", "sonarqube");
+ }
+
+ @Test
+ public void does_not_match_partial_key() {
+ index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("some name").setDbKey("theKey")));
+
+ assertNoResults("theke");
+ assertNoResults("hekey");
+ }
+
+ @Test
+ public void facets_take_into_account_text_search() {
+ index(
+ // docs with ncloc<1K
+ newDoc(newPrivateProjectDto(ORG).setName("Windows").setDbKey("project1"), NCLOC, 0d),
+ newDoc(newPrivateProjectDto(ORG).setName("apachee").setDbKey("project2"), NCLOC, 999d),
+ // docs with ncloc>=1K and ncloc<10K
+ newDoc(newPrivateProjectDto(ORG).setName("Apache").setDbKey("project3"), NCLOC, 1_000d),
+ // docs with ncloc>=100K and ncloc<500K
+ newDoc(newPrivateProjectDto(ORG).setName("Apache Foundation").setDbKey("project4"), NCLOC, 100_000d));
+
+ assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache"), 1L, 1L, 0L, 1L, 0L);
+ assertNclocFacet(new ProjectMeasuresQuery().setQueryText("PAch"), 1L, 1L, 0L, 1L, 0L);
+ assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache foundation"), 0L, 0L, 0L, 1L, 0L);
+ assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project3"), 0L, 1L, 0L, 0L, 0L);
+ assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project"), 0L, 0L, 0L, 0L, 0L);
+ }
+
+ @Test
+ public void filter_by_metric_take_into_account_text_search() {
+ index(
+ newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1"), NCLOC, 30_000d),
+ newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2"), NCLOC, 40_000d),
+ newDoc(newPrivateProjectDto(ORG).setUuid("project3").setName("Apache").setDbKey("project3"), NCLOC, 50_000d),
+ newDoc(newPrivateProjectDto(ORG).setUuid("project4").setName("Apache").setDbKey("project4"), NCLOC, 60_000d));
+
+ assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d)), "project3", "project4", "project2");
+ assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2");
+ assertResults(new ProjectMeasuresQuery().setQueryText("PAC").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2");
+ assertResults(new ProjectMeasuresQuery().setQueryText("apachee").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 30_000d)), "project2");
+ assertResults(new ProjectMeasuresQuery().setQueryText("unknown").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d)));
+ }
+
+ private void index(ProjectMeasuresDoc... docs) {
+ es.putDocuments(TYPE_PROJECT_MEASURES, docs);
+ authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).allowAnyone()).collect(toList()));
+ }
+
+ private static ProjectMeasuresDoc newDoc(ComponentDto project) {
+ return new ProjectMeasuresDoc()
+ .setOrganizationUuid(project.getOrganizationUuid())
+ .setId(project.uuid())
+ .setKey(project.getDbKey())
+ .setName(project.name());
+ }
+
+ private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) {
+ return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1)));
+ }
+
+ private static Map<String, Object> newMeasure(String key, Object value) {
+ return ImmutableMap.of("key", key, "value", value);
+ }
+
+ private void assertResults(ProjectMeasuresQuery query, String... expectedProjectUuids) {
+ List<String> result = underTest.search(query, new SearchOptions()).getIds();
+ assertThat(result).containsExactly(expectedProjectUuids);
+ }
+
+ private void assertTextQueryResults(String queryText, String... expectedProjectUuids) {
+ assertResults(new ProjectMeasuresQuery().setQueryText(queryText), expectedProjectUuids);
+ }
+
+ private void assertNoResults(String queryText) {
+ assertTextQueryResults(queryText);
+ }
+
+ private void assertNclocFacet(ProjectMeasuresQuery query, Long... facetExpectedValues) {
+ checkArgument(facetExpectedValues.length == 5, "5 facet values is required");
+ Facets facets = underTest.search(query, new SearchOptions().addFacets(NCLOC)).getFacets();
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", facetExpectedValues[0]),
+ entry("1000.0-10000.0", facetExpectedValues[1]),
+ entry("10000.0-100000.0", facetExpectedValues[2]),
+ entry("100000.0-500000.0", facetExpectedValues[3]),
+ entry("500000.0-*", facetExpectedValues[4]));
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java
new file mode 100644
index 00000000000..4d1fca0083f
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.measure.index;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.measures.Metric.Level;
+import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.sonar.api.measures.Metric.Level.OK;
+import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.EQ;
+
+public class ProjectMeasuresQueryTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private ProjectMeasuresQuery underTest = new ProjectMeasuresQuery();
+
+ @Test
+ public void empty_query() {
+ assertThat(underTest.getMetricCriteria()).isEmpty();
+ assertThat(underTest.getQualityGateStatus()).isEmpty();
+ assertThat(underTest.getOrganizationUuid()).isEmpty();
+ }
+
+ @Test
+ public void add_metric_criterion() {
+ underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d));
+
+ assertThat(underTest.getMetricCriteria())
+ .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue)
+ .containsOnly(tuple("coverage", EQ, 10d));
+ }
+
+ @Test
+ public void isNoData_returns_true_when_no_data() {
+ underTest.addMetricCriterion(MetricCriterion.createNoData("coverage"));
+
+ assertThat(underTest.getMetricCriteria())
+ .extracting(MetricCriterion::getMetricKey, MetricCriterion::isNoData)
+ .containsOnly(tuple("coverage", true));
+ }
+
+ @Test
+ public void isNoData_returns_false_when_data_exists() {
+ underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d));
+
+ assertThat(underTest.getMetricCriteria())
+ .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::isNoData)
+ .containsOnly(tuple("coverage", EQ, false));
+ }
+
+ @Test
+ public void set_quality_gate_status() {
+ underTest.setQualityGateStatus(OK);
+
+ assertThat(underTest.getQualityGateStatus().get()).isEqualTo(Level.OK);
+ }
+
+ @Test
+ public void default_sort_is_by_name() {
+ assertThat(underTest.getSort()).isEqualTo("name");
+ }
+
+ @Test
+ public void fail_to_set_null_sort() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("Sort cannot be null");
+
+ underTest.setSort(null);
+ }
+
+ @Test
+ public void fail_to_get_value_when_no_data() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("The criterion for metric coverage has no data");
+
+ MetricCriterion.createNoData("coverage").getValue();
+ }
+
+ @Test
+ public void fail_to_get_operator_when_no_data() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("The criterion for metric coverage has no data");
+
+ MetricCriterion.createNoData("coverage").getOperator();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java
new file mode 100644
index 00000000000..3b460edc827
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.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.measure.index;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class ProjectsEsModuleTest {
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new ProjectsEsModule().configure(container);
+ assertThat(container.size()).isEqualTo(3 + COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationChannelTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationChannelTest.java
new file mode 100644
index 00000000000..9d90900df71
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationChannelTest.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.notification;
+
+import org.junit.Test;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.notifications.NotificationChannel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NotificationChannelTest {
+
+ @Test
+ public void defaultMethods() {
+ NotificationChannel channel = new FakeNotificationChannel();
+ assertThat(channel.getKey()).isEqualTo("FakeNotificationChannel");
+ assertThat(channel.toString()).isEqualTo("FakeNotificationChannel");
+ }
+
+ private class FakeNotificationChannel extends NotificationChannel {
+ @Override
+ public boolean deliver(Notification notification, String username) {
+ return true;
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java
new file mode 100644
index 00000000000..8a24e6b092c
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationDaemonTest.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.notification;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.mockito.verification.Timeout;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.notifications.Notification;
+
+import static java.util.Collections.singleton;
+import static org.mockito.ArgumentMatchers.anyCollection;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.when;
+
+public class NotificationDaemonTest {
+ private DefaultNotificationManager manager = mock(DefaultNotificationManager.class);
+ private NotificationService notificationService = mock(NotificationService.class);
+ private NotificationDaemon underTest;
+ private InOrder inOrder;
+
+ @Before
+ public void setUp() throws Exception {
+ MapSettings settings = new MapSettings(new PropertyDefinitions(NotificationDaemon.class)).setProperty("sonar.notifications.delay", 1L);
+
+ underTest = new NotificationDaemon(settings.asConfig(), manager, notificationService);
+ inOrder = Mockito.inOrder(notificationService);
+ }
+
+ @After
+ public void tearDown() {
+ underTest.stop();
+ }
+
+ @Test
+ public void no_effect_when_no_notification() {
+ when(manager.getFromQueue()).thenReturn(null);
+
+ underTest.start();
+ inOrder.verify(notificationService, new Timeout(2000, Mockito.times(0))).deliverEmails(anyCollection());
+ inOrder.verifyNoMoreInteractions();
+ underTest.stop();
+ }
+
+ @Test
+ public void calls_both_api_and_deprecated_API() {
+ Notification notification = mock(Notification.class);
+ when(manager.getFromQueue()).thenReturn(notification).thenReturn(null);
+
+ underTest.start();
+ inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification));
+ inOrder.verify(notificationService).deliver(notification);
+ inOrder.verifyNoMoreInteractions();
+ underTest.stop();
+ }
+
+ @Test
+ public void notifications_are_processed_one_by_one_even_with_new_API() {
+ Notification notification1 = mock(Notification.class);
+ Notification notification2 = mock(Notification.class);
+ Notification notification3 = mock(Notification.class);
+ Notification notification4 = mock(Notification.class);
+ when(manager.getFromQueue())
+ .thenReturn(notification1)
+ .thenReturn(notification2)
+ .thenReturn(notification3)
+ .thenReturn(notification4)
+ .thenReturn(null);
+
+ underTest.start();
+ inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification1));
+ inOrder.verify(notificationService).deliver(notification1);
+ inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification2));
+ inOrder.verify(notificationService).deliver(notification2);
+ inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification3));
+ inOrder.verify(notificationService).deliver(notification3);
+ inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification4));
+ inOrder.verify(notificationService).deliver(notification4);
+ inOrder.verifyNoMoreInteractions();
+ underTest.stop();
+
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationMediumTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationMediumTest.java
new file mode 100644
index 00000000000..9476a84c983
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationMediumTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.notification;
+
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.db.DbClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class NotificationMediumTest {
+ private static String CREATOR_SIMON = "simon";
+ private static String CREATOR_EVGENY = "evgeny";
+ private static String ASSIGNEE_SIMON = "simon";
+
+ private DefaultNotificationManager manager = mock(DefaultNotificationManager.class);
+ private Notification notification = mock(Notification.class);
+ private NotificationChannel emailChannel = mock(NotificationChannel.class);
+ private NotificationChannel gtalkChannel = mock(NotificationChannel.class);
+ private NotificationDispatcher commentOnIssueAssignedToMe = mock(NotificationDispatcher.class);
+ private NotificationDispatcher commentOnIssueCreatedByMe = mock(NotificationDispatcher.class);
+ private NotificationDispatcher qualityGateChange = mock(NotificationDispatcher.class);
+ private DbClient dbClient = mock(DbClient.class);
+ private NotificationService service = new NotificationService(dbClient, new NotificationDispatcher[] {commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange});
+ private NotificationDaemon underTest = null;
+
+ private void setUpMocks() {
+ when(emailChannel.getKey()).thenReturn("email");
+ when(gtalkChannel.getKey()).thenReturn("gtalk");
+ when(commentOnIssueAssignedToMe.getKey()).thenReturn("CommentOnIssueAssignedToMe");
+ when(commentOnIssueAssignedToMe.getType()).thenReturn("issue-changes");
+ when(commentOnIssueCreatedByMe.getKey()).thenReturn("CommentOnIssueCreatedByMe");
+ when(commentOnIssueCreatedByMe.getType()).thenReturn("issue-changes");
+ when(qualityGateChange.getKey()).thenReturn("QGateChange");
+ when(qualityGateChange.getType()).thenReturn("qgate-changes");
+ when(manager.getFromQueue()).thenReturn(notification).thenReturn(null);
+
+ MapSettings settings = new MapSettings(new PropertyDefinitions(NotificationDaemon.class)).setProperty("sonar.notifications.delay", 1L);
+
+ underTest = new NotificationDaemon(settings.asConfig(), manager, service);
+ }
+
+ /**
+ * Given:
+ * Simon wants to receive notifications by email on comments for reviews assigned to him or created by him.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Simon and assigned to Simon.
+ * <p/>
+ * Then:
+ * Only one notification should be delivered to Simon by Email.
+ */
+ @Test
+ public void scenario1() {
+ setUpMocks();
+ doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+ doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+
+ underTest.start();
+ verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON);
+ underTest.stop();
+
+ verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON);
+ }
+
+ /**
+ * Given:
+ * Evgeny wants to receive notification by GTalk on comments for reviews created by him.
+ * Simon wants to receive notification by Email on comments for reviews assigned to him.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Evgeny and assigned to Simon.
+ * <p/>
+ * Then:
+ * Two notifications should be delivered - one to Simon by Email and another to Evgeny by GTalk.
+ */
+ @Test
+ public void scenario2() {
+ setUpMocks();
+ doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+ doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+
+ underTest.start();
+ verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON);
+ verify(gtalkChannel, timeout(2000)).deliver(notification, CREATOR_EVGENY);
+ underTest.stop();
+
+ verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY);
+ verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON);
+ }
+
+ /**
+ * Given:
+ * Simon wants to receive notifications by Email and GTLak on comments for reviews assigned to him.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Evgeny and assigned to Simon.
+ * <p/>
+ * Then:
+ * Two notifications should be delivered to Simon - one by Email and another by GTalk.
+ */
+ @Test
+ public void scenario3() {
+ setUpMocks();
+ doAnswer(addUser(ASSIGNEE_SIMON, new NotificationChannel[] {emailChannel, gtalkChannel}))
+ .when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+
+ underTest.start();
+ verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON);
+ verify(gtalkChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON);
+ underTest.stop();
+
+ verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY);
+ verify(gtalkChannel, never()).deliver(notification, CREATOR_EVGENY);
+ }
+
+ /**
+ * Given:
+ * Nobody wants to receive notifications.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Evgeny and assigned to Simon.
+ * <p/>
+ * Then:
+ * No notifications.
+ */
+ @Test
+ public void scenario4() {
+ setUpMocks();
+
+ underTest.start();
+ underTest.stop();
+
+ verify(emailChannel, never()).deliver(any(Notification.class), anyString());
+ verify(gtalkChannel, never()).deliver(any(Notification.class), anyString());
+ }
+
+ // SONAR-4548
+ @Test
+ public void shouldNotStopWhenException() {
+ setUpMocks();
+ when(manager.getFromQueue()).thenThrow(new RuntimeException("Unexpected exception")).thenReturn(notification).thenReturn(null);
+ doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+ doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+
+ underTest.start();
+ verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON);
+ underTest.stop();
+
+ verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON);
+ }
+
+ @Test
+ public void shouldNotAddNullAsUser() {
+ setUpMocks();
+ doAnswer(addUser(null, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class));
+
+ underTest.start();
+ underTest.stop();
+
+ verify(emailChannel, never()).deliver(any(Notification.class), anyString());
+ verify(gtalkChannel, never()).deliver(any(Notification.class), anyString());
+ }
+
+ @Test
+ public void getDispatchers() {
+ setUpMocks();
+
+ assertThat(service.getDispatchers()).containsOnly(commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange);
+ }
+
+ @Test
+ public void getDispatchers_empty() {
+ Settings settings = new MapSettings().setProperty("sonar.notifications.delay", 1L);
+
+ service = new NotificationService(dbClient);
+ assertThat(service.getDispatchers()).hasSize(0);
+ }
+
+ @Test
+ public void shouldLogEvery10Minutes() {
+ setUpMocks();
+ // Emulate 2 notifications in DB
+ when(manager.getFromQueue()).thenReturn(notification).thenReturn(notification).thenReturn(null);
+ when(manager.count()).thenReturn(1L).thenReturn(0L);
+ underTest = spy(underTest);
+ // Emulate processing of each notification take 10 min to have a log each time
+ when(underTest.now()).thenReturn(0L).thenReturn(10 * 60 * 1000 + 1L).thenReturn(20 * 60 * 1000 + 2L);
+ underTest.start();
+ verify(underTest, timeout(200)).log(0, 1, 10);
+ verify(underTest, timeout(200)).log(0, 0, 20);
+ underTest.stop();
+ }
+
+ private static Answer<Object> addUser(final String user, final NotificationChannel channel) {
+ return addUser(user, new NotificationChannel[] {channel});
+ }
+
+ private static Answer<Object> addUser(final String user, final NotificationChannel[] channels) {
+ return new Answer<Object>() {
+ public Object answer(InvocationOnMock invocation) {
+ for (NotificationChannel channel : channels) {
+ ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel);
+ }
+ return null;
+ }
+ };
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationModuleTest.java
new file mode 100644
index 00000000000..b485602fcd3
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationModuleTest.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.notification;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class NotificationModuleTest {
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new NotificationModule().configure(container);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 5);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationTest.java
new file mode 100644
index 00000000000..bc3400d44c6
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.notification;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.notifications.Notification;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NotificationTest {
+
+ private Notification notification;
+
+ @Before
+ public void init() {
+ notification = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42");
+ }
+
+ @Test
+ public void shouldReturnType() {
+ assertThat(notification.getType()).isEqualTo("alerts");
+ }
+
+ @Test
+ public void shouldReturnDefaultMessage() {
+ assertThat(notification.getDefaultMessage()).isEqualTo("There are new alerts");
+ }
+
+ @Test
+ public void shouldReturnToStringIfDefaultMessageNotSet() {
+ notification = new Notification("alerts").setFieldValue("alertCount", "42");
+ System.out.println(notification);
+ assertThat(notification.getDefaultMessage()).contains("type='alerts'");
+ assertThat(notification.getDefaultMessage()).contains("fields={alertCount=42}");
+ }
+
+ @Test
+ public void shouldReturnField() {
+ assertThat(notification.getFieldValue("alertCount")).isEqualTo("42");
+ assertThat(notification.getFieldValue("fake")).isNull();
+
+ // default message is stored as field as well
+ assertThat(notification.getFieldValue("default_message")).isEqualTo("There are new alerts");
+ }
+
+ @Test
+ public void shouldEqual() {
+ assertThat(notification.equals("")).isFalse();
+ assertThat(notification.equals(null)).isFalse();
+ assertThat(notification.equals(notification)).isTrue();
+
+ Notification otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42");
+ assertThat(otherNotif).isEqualTo(notification);
+
+ otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "15000");
+ assertThat(otherNotif).isNotEqualTo(notification);
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java
new file mode 100644
index 00000000000..7b57c9af448
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java
@@ -0,0 +1,369 @@
+/*
+ * 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.notification.email;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import org.apache.commons.mail.EmailException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.issue.notification.EmailMessage;
+import org.sonar.server.issue.notification.EmailTemplate;
+import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
+import org.subethamail.wiser.Wiser;
+import org.subethamail.wiser.WiserMessage;
+
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+import static junit.framework.Assert.fail;
+import static org.apache.commons.lang.RandomStringUtils.random;
+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;
+
+@RunWith(DataProviderRunner.class)
+public class EmailNotificationChannelTest {
+
+ private static final String SUBJECT_PREFIX = "[SONARQUBE]";
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private Wiser smtpServer;
+ private EmailSettings configuration;
+ private EmailNotificationChannel underTest;
+
+ @Before
+ public void setUp() {
+ smtpServer = new Wiser(0);
+ smtpServer.start();
+
+ configuration = mock(EmailSettings.class);
+ underTest = new EmailNotificationChannel(configuration, null, null);
+ }
+
+ @After
+ public void tearDown() {
+ smtpServer.stop();
+ }
+
+ @Test
+ public void isActivated_returns_true_if_smpt_host_is_not_empty() {
+ when(configuration.getSmtpHost()).thenReturn(random(5));
+
+ assertThat(underTest.isActivated()).isTrue();
+ }
+
+ @Test
+ public void isActivated_returns_false_if_smpt_host_is_null() {
+ when(configuration.getSmtpHost()).thenReturn(null);
+
+ assertThat(underTest.isActivated()).isFalse();
+ }
+
+ @Test
+ public void isActivated_returns_false_if_smpt_host_is_empty() {
+ when(configuration.getSmtpHost()).thenReturn("");
+
+ assertThat(underTest.isActivated()).isFalse();
+ }
+
+ @Test
+ public void shouldSendTestEmail() throws Exception {
+ configure();
+ underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
+
+ List<WiserMessage> messages = smtpServer.getMessages();
+ assertThat(messages).hasSize(1);
+
+ MimeMessage email = messages.get(0).getMimeMessage();
+ assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
+ assertThat(email.getHeader("From", ",")).isEqualTo("SonarQube from NoWhere <server@nowhere>");
+ assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
+ assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Test Message from SonarQube");
+ assertThat((String) email.getContent()).startsWith("This is a test message from SonarQube.");
+ }
+
+ @Test
+ public void shouldThrowAnExceptionWhenUnableToSendTestEmail() {
+ configure();
+ smtpServer.stop();
+
+ try {
+ underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
+ fail();
+ } catch (EmailException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void shouldNotSendEmailWhenHostnameNotConfigured() {
+ EmailMessage emailMessage = new EmailMessage()
+ .setTo("user@nowhere")
+ .setSubject("Foo")
+ .setPlainTextMessage("Bar");
+ boolean delivered = underTest.deliver(emailMessage);
+ assertThat(smtpServer.getMessages()).isEmpty();
+ assertThat(delivered).isFalse();
+ }
+
+ @Test
+ public void shouldSendThreadedEmail() throws Exception {
+ configure();
+ EmailMessage emailMessage = new EmailMessage()
+ .setMessageId("reviews/view/1")
+ .setFrom("Full Username")
+ .setTo("user@nowhere")
+ .setSubject("Review #3")
+ .setPlainTextMessage("I'll take care of this violation.");
+ boolean delivered = underTest.deliver(emailMessage);
+
+ List<WiserMessage> messages = smtpServer.getMessages();
+ assertThat(messages).hasSize(1);
+
+ MimeMessage email = messages.get(0).getMimeMessage();
+
+ assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
+
+ assertThat(email.getHeader("In-Reply-To", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>");
+ assertThat(email.getHeader("References", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>");
+
+ assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>");
+ assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org");
+
+ assertThat(email.getHeader("From", ",")).isEqualTo("\"Full Username (SonarQube from NoWhere)\" <server@nowhere>");
+ assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
+ assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Review #3");
+ assertThat((String) email.getContent()).startsWith("I'll take care of this violation.");
+ assertThat(delivered).isTrue();
+ }
+
+ @Test
+ public void shouldSendNonThreadedEmail() throws Exception {
+ configure();
+ EmailMessage emailMessage = new EmailMessage()
+ .setTo("user@nowhere")
+ .setSubject("Foo")
+ .setPlainTextMessage("Bar");
+ boolean delivered = underTest.deliver(emailMessage);
+
+ List<WiserMessage> messages = smtpServer.getMessages();
+ assertThat(messages).hasSize(1);
+
+ MimeMessage email = messages.get(0).getMimeMessage();
+
+ assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
+
+ assertThat(email.getHeader("In-Reply-To", null)).isNull();
+ assertThat(email.getHeader("References", null)).isNull();
+
+ assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>");
+ assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org");
+
+ assertThat(email.getHeader("From", null)).isEqualTo("SonarQube from NoWhere <server@nowhere>");
+ assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
+ assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Foo");
+ assertThat((String) email.getContent()).startsWith("Bar");
+ assertThat(delivered).isTrue();
+ }
+
+ @Test
+ public void shouldNotThrowAnExceptionWhenUnableToSendEmail() {
+ configure();
+ smtpServer.stop();
+
+ EmailMessage emailMessage = new EmailMessage()
+ .setTo("user@nowhere")
+ .setSubject("Foo")
+ .setPlainTextMessage("Bar");
+ boolean delivered = underTest.deliver(emailMessage);
+
+ assertThat(delivered).isFalse();
+ }
+
+ @Test
+ public void shouldSendTestEmailWithSTARTTLS() {
+ smtpServer.getServer().setEnableTLS(true);
+ smtpServer.getServer().setRequireTLS(true);
+ configure();
+ when(configuration.getSecureConnection()).thenReturn("STARTTLS");
+
+ try {
+ underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
+ fail("An SSL exception was expected a a proof that STARTTLS is enabled");
+ } catch (EmailException e) {
+ // We don't have a SSL certificate so we are expecting a SSL error
+ assertThat(e.getCause().getMessage()).isEqualTo("Could not convert socket to TLS");
+ }
+ }
+
+ @Test
+ public void deliverAll_has_no_effect_if_set_is_empty() {
+ EmailSettings emailSettings = mock(EmailSettings.class);
+ EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null);
+
+ int count = underTest.deliverAll(Collections.emptySet());
+
+ assertThat(count).isZero();
+ verifyZeroInteractions(emailSettings);
+ assertThat(smtpServer.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void deliverAll_has_no_effect_if_smtp_host_is_null() {
+ EmailSettings emailSettings = mock(EmailSettings.class);
+ when(emailSettings.getSmtpHost()).thenReturn(null);
+ Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
+ .mapToObj(i -> new EmailDeliveryRequest("foo" + i + "@moo", mock(Notification.class)))
+ .collect(toSet());
+ EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null);
+
+ int count = underTest.deliverAll(requests);
+
+ assertThat(count).isZero();
+ verify(emailSettings).getSmtpHost();
+ verifyNoMoreInteractions(emailSettings);
+ assertThat(smtpServer.getMessages()).isEmpty();
+ }
+
+ @Test
+ @UseDataProvider("emptyStrings")
+ public void deliverAll_ignores_requests_which_recipient_is_empty(String emptyString) {
+ EmailSettings emailSettings = mock(EmailSettings.class);
+ when(emailSettings.getSmtpHost()).thenReturn(null);
+ Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
+ .mapToObj(i -> new EmailDeliveryRequest(emptyString, mock(Notification.class)))
+ .collect(toSet());
+ EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null);
+
+ int count = underTest.deliverAll(requests);
+
+ assertThat(count).isZero();
+ verify(emailSettings).getSmtpHost();
+ verifyNoMoreInteractions(emailSettings);
+ assertThat(smtpServer.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void deliverAll_returns_count_of_request_for_which_at_least_one_formatter_accept_it() throws MessagingException, IOException {
+ String recipientEmail = "foo@donut";
+ configure();
+ Notification notification1 = mock(Notification.class);
+ Notification notification2 = mock(Notification.class);
+ Notification notification3 = mock(Notification.class);
+ EmailTemplate template1 = mock(EmailTemplate.class);
+ EmailTemplate template3 = mock(EmailTemplate.class);
+ EmailMessage emailMessage1 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11");
+ EmailMessage emailMessage3 = new EmailMessage().setTo(recipientEmail).setSubject("sub3").setPlainTextMessage("msg3");
+ when(template1.format(notification1)).thenReturn(emailMessage1);
+ when(template3.format(notification3)).thenReturn(emailMessage3);
+ Set<EmailDeliveryRequest> requests = Stream.of(notification1, notification2, notification3)
+ .map(t -> new EmailDeliveryRequest(recipientEmail, t))
+ .collect(toSet());
+ EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, new EmailTemplate[] {template1, template3}, null);
+
+ int count = underTest.deliverAll(requests);
+
+ assertThat(count).isEqualTo(2);
+ assertThat(smtpServer.getMessages()).hasSize(2);
+ Map<String, MimeMessage> messagesBySubject = smtpServer.getMessages().stream()
+ .map(t -> {
+ try {
+ return t.getMimeMessage();
+ } catch (MessagingException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .collect(toMap(t -> {
+ try {
+ return t.getSubject();
+ } catch (MessagingException e) {
+ throw new RuntimeException(e);
+ }
+ }, t -> t));
+
+ assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage1.getSubject()).getContent())
+ .contains(emailMessage1.getMessage());
+ assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage3.getSubject()).getContent())
+ .contains(emailMessage3.getMessage());
+ }
+
+ @Test
+ public void deliverAll_ignores_multiple_templates_by_notification_and_takes_the_first_one_only() throws MessagingException, IOException {
+ String recipientEmail = "foo@donut";
+ configure();
+ Notification notification1 = mock(Notification.class);
+ EmailTemplate template11 = mock(EmailTemplate.class);
+ EmailTemplate template12 = mock(EmailTemplate.class);
+ EmailMessage emailMessage11 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11");
+ EmailMessage emailMessage12 = new EmailMessage().setTo(recipientEmail).setSubject("sub12").setPlainTextMessage("msg12");
+ when(template11.format(notification1)).thenReturn(emailMessage11);
+ when(template12.format(notification1)).thenReturn(emailMessage12);
+ EmailDeliveryRequest request = new EmailDeliveryRequest(recipientEmail, notification1);
+ EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, new EmailTemplate[] {template11, template12}, null);
+
+ int count = underTest.deliverAll(Collections.singleton(request));
+
+ assertThat(count).isEqualTo(1);
+ assertThat(smtpServer.getMessages()).hasSize(1);
+ assertThat((String) smtpServer.getMessages().iterator().next().getMimeMessage().getContent())
+ .contains(emailMessage11.getMessage());
+ }
+
+ @DataProvider
+ public static Object[][] emptyStrings() {
+ return new Object[][] {
+ {""},
+ {" "},
+ {" \n "}
+ };
+ }
+
+ private void configure() {
+ when(configuration.getSmtpHost()).thenReturn("localhost");
+ when(configuration.getSmtpPort()).thenReturn(smtpServer.getServer().getPort());
+ when(configuration.getFrom()).thenReturn("server@nowhere");
+ when(configuration.getFromName()).thenReturn("SonarQube from NoWhere");
+ when(configuration.getPrefix()).thenReturn(SUBJECT_PREFIX);
+ when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org");
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/BackendCleanupTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/BackendCleanupTest.java
new file mode 100644
index 00000000000..30c65d1fee0
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/BackendCleanupTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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 java.util.Random;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.organization.OrganizationTesting;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.component.index.ComponentDoc;
+import org.sonar.server.component.index.ComponentIndexDefinition;
+import org.sonar.server.component.index.ComponentIndexer;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.issue.IssueDocTesting;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.measure.index.ProjectMeasuresDoc;
+import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition;
+import org.sonar.server.rule.index.RuleDoc;
+import org.sonar.server.rule.index.RuleIndexDefinition;
+import org.sonar.server.view.index.ViewDoc;
+import org.sonar.server.view.index.ViewIndexDefinition;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BackendCleanupTest {
+
+ @Rule
+ public EsTester es = EsTester.create();
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ private BackendCleanup underTest = new BackendCleanup(es.client(), dbTester.getDbClient());
+ private OrganizationDto organization;
+
+ @Before
+ public void setUp() {
+ organization = OrganizationTesting.newOrganizationDto();
+ }
+
+ @Test
+ public void clear_db() {
+ insertSomeData();
+
+ underTest.clearDb();
+
+ assertThat(dbTester.countRowsOfTable("projects")).isEqualTo(0);
+ assertThat(dbTester.countRowsOfTable("snapshots")).isEqualTo(0);
+ assertThat(dbTester.countRowsOfTable("rules")).isEqualTo(0);
+ assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
+ }
+
+ @Test
+ public void clear_indexes() {
+ es.putDocuments(IssueIndexDefinition.TYPE_ISSUE, IssueDocTesting.newDoc());
+ es.putDocuments(RuleIndexDefinition.TYPE_RULE, newRuleDoc());
+ es.putDocuments(ComponentIndexDefinition.TYPE_COMPONENT, newComponentDoc());
+
+ underTest.clearIndexes();
+
+ assertThat(es.countDocuments(IssueIndexDefinition.TYPE_ISSUE)).isEqualTo(0);
+ assertThat(es.countDocuments(ComponentIndexDefinition.TYPE_COMPONENT)).isEqualTo(0);
+ }
+
+ @Test
+ public void clear_all() {
+ insertSomeData();
+
+ es.putDocuments(IssueIndexDefinition.TYPE_ISSUE, IssueDocTesting.newDoc());
+ es.putDocuments(RuleIndexDefinition.TYPE_RULE, newRuleDoc());
+ es.putDocuments(ComponentIndexDefinition.TYPE_COMPONENT, newComponentDoc());
+
+ underTest.clearAll();
+
+ assertThat(es.countDocuments(IssueIndexDefinition.TYPE_ISSUE)).isEqualTo(0);
+ assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(0);
+ assertThat(es.countDocuments(ComponentIndexDefinition.TYPE_COMPONENT)).isEqualTo(0);
+
+ assertThat(dbTester.countRowsOfTable("projects")).isEqualTo(0);
+ assertThat(dbTester.countRowsOfTable("snapshots")).isEqualTo(0);
+ assertThat(dbTester.countRowsOfTable("rules")).isEqualTo(0);
+ assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
+ }
+
+ @Test
+ public void reset_data() {
+ insertSomeData();
+
+ es.putDocuments(IssueIndexDefinition.TYPE_ISSUE, IssueDocTesting.newDoc());
+ es.putDocuments(ViewIndexDefinition.TYPE_VIEW, new ViewDoc().setUuid("CDEF").setProjects(newArrayList("DEFG")));
+ es.putDocuments(RuleIndexDefinition.TYPE_RULE, newRuleDoc());
+ es.putDocuments(ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES, new ProjectMeasuresDoc()
+ .setId("PROJECT")
+ .setKey("Key")
+ .setName("Name"));
+ es.putDocuments(ComponentIndexDefinition.TYPE_COMPONENT, newComponentDoc());
+
+ underTest.resetData();
+
+ assertThat(dbTester.countRowsOfTable("projects")).isZero();
+ assertThat(dbTester.countRowsOfTable("snapshots")).isZero();
+ assertThat(dbTester.countRowsOfTable("properties")).isZero();
+ assertThat(es.countDocuments(IssueIndexDefinition.TYPE_ISSUE)).isZero();
+ assertThat(es.countDocuments(ViewIndexDefinition.TYPE_VIEW)).isZero();
+ assertThat(es.countDocuments(ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES)).isZero();
+ assertThat(es.countDocuments(ComponentIndexDefinition.TYPE_COMPONENT)).isZero();
+
+ // Rules should not be removed
+ assertThat(dbTester.countRowsOfTable("rules")).isEqualTo(1);
+ assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(1);
+ }
+
+ private void insertSomeData() {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto project = dbTester.components().insertPrivateProject(organization);
+ dbTester.components().insertSnapshot(project);
+ dbTester.rules().insert();
+ dbTester.properties().insertProperty(new PropertyDto()
+ .setKey("sonar.profile.java")
+ .setValue("Sonar Way")
+ .setResourceId(project.getId())
+ );
+ }
+
+ private static RuleDoc newRuleDoc() {
+ return new RuleDoc().setId(new Random().nextInt(942)).setKey(RuleTesting.XOO_X1.toString()).setRepository(RuleTesting.XOO_X1.repository());
+ }
+
+ private ComponentDoc newComponentDoc() {
+ return ComponentIndexer.toDocument(ComponentTesting.newPrivateProjectDto(organization));
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClassLoaderUtilsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClassLoaderUtilsTest.java
new file mode 100644
index 00000000000..f81955071ec
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClassLoaderUtilsTest.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.platform;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.server.util.ClassLoaderUtils;
+
+import static org.apache.commons.lang.StringUtils.endsWith;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ClassLoaderUtilsTest {
+
+ private ClassLoader classLoader;
+
+ @Before
+ public void prepareClassLoader() {
+ // This JAR file has the three following files :
+ // org/sonar/sqale/app/copyright.txt
+ // org/sonar/sqale/app/README.md
+ // org/sonar/other/other.txt
+ URL jarUrl = getClass().getResource("/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar");
+ classLoader = new URLClassLoader(new URL[] {jarUrl}, /* no parent classloader */null);
+ }
+
+ @Test
+ public void listResources_unknown_root() {
+ Collection<String> strings = ClassLoaderUtils.listResources(classLoader, "unknown/directory", s -> true);
+ assertThat(strings).isEmpty();
+ }
+
+ @Test
+ public void listResources_all() {
+ Collection<String> strings = ClassLoaderUtils.listResources(classLoader, "org/sonar/sqale", s -> true);
+ assertThat(strings).containsOnly(
+ "org/sonar/sqale/",
+ "org/sonar/sqale/app/",
+ "org/sonar/sqale/app/copyright.txt",
+ "org/sonar/sqale/app/README.md");
+ }
+
+ @Test
+ public void listResources_use_predicate() {
+ Collection<String> strings = ClassLoaderUtils.listResources(classLoader, "org/sonar/sqale", s -> endsWith(s, "md"));
+ assertThat(strings).containsOnly("org/sonar/sqale/app/README.md");
+ }
+
+ @Test
+ public void listFiles() {
+ Collection<String> strings = ClassLoaderUtils.listFiles(classLoader, "org/sonar/sqale");
+ assertThat(strings).containsOnly(
+ "org/sonar/sqale/app/copyright.txt",
+ "org/sonar/sqale/app/README.md");
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterVerificationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterVerificationTest.java
new file mode 100644
index 00000000000..590bca2cb09
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterVerificationTest.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.platform;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ClusterVerificationTest {
+
+ private static final String ERROR_MESSAGE = "Cluster mode can't be enabled. Please install the Data Center Edition. More details at https://redirect.sonarsource.com/editions/datacenter.html.";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private WebServer webServer = mock(WebServer.class);
+ private ClusterFeature feature = mock(ClusterFeature.class);
+
+ @Test
+ public void throw_MessageException_if_cluster_is_enabled_but_HA_plugin_is_not_installed() {
+ when(webServer.isStandalone()).thenReturn(false);
+
+ ClusterVerification underTest = new ClusterVerification(webServer);
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(ERROR_MESSAGE);
+ underTest.start();
+ }
+
+ @Test
+ public void throw_MessageException_if_cluster_is_enabled_but_HA_feature_is_not_enabled() {
+ when(webServer.isStandalone()).thenReturn(false);
+ when(feature.isEnabled()).thenReturn(false);
+ ClusterVerification underTest = new ClusterVerification(webServer, feature);
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(ERROR_MESSAGE);
+ underTest.start();
+ }
+
+ @Test
+ public void do_not_fail_if_cluster_is_enabled_and_HA_feature_is_enabled() {
+ when(webServer.isStandalone()).thenReturn(false);
+ when(feature.isEnabled()).thenReturn(true);
+ ClusterVerification underTest = new ClusterVerification(webServer, feature);
+
+ // no failure
+ underTest.start();
+ underTest.stop();
+ }
+
+ @Test
+ public void do_not_fail_if_cluster_is_disabled() {
+ when(webServer.isStandalone()).thenReturn(true);
+
+ ClusterVerification underTest = new ClusterVerification(webServer);
+
+ // no failure
+ underTest.start();
+ underTest.stop();
+ }
+
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DatabaseServerCompatibilityTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DatabaseServerCompatibilityTest.java
new file mode 100644
index 00000000000..3de7281760b
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DatabaseServerCompatibilityTest.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.platform;
+
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DatabaseServerCompatibilityTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public LogTester logTester = new LogTester();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ private MapSettings settings = new MapSettings();
+
+ @Test
+ public void fail_if_requires_downgrade() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("Database was upgraded to a more recent version of SonarQube. "
+ + "A backup must probably be restored or the DB settings are incorrect.");
+
+ DatabaseVersion version = mock(DatabaseVersion.class);
+ when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_DOWNGRADE);
+ new DatabaseServerCompatibility(version, settings.asConfig()).start();
+ }
+
+ @Test
+ public void fail_if_requires_firstly_to_upgrade_to_lts() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("Current version is too old. Please upgrade to Long Term Support version firstly.");
+
+ DatabaseVersion version = mock(DatabaseVersion.class);
+ when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);
+ when(version.getVersion()).thenReturn(Optional.of(12L));
+ new DatabaseServerCompatibility(version, settings.asConfig()).start();
+ }
+
+ @Test
+ public void log_warning_if_requires_upgrade() {
+ DatabaseVersion version = mock(DatabaseVersion.class);
+ when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);
+ when(version.getVersion()).thenReturn(Optional.of(DatabaseVersion.MIN_UPGRADE_VERSION));
+ new DatabaseServerCompatibility(version, settings.asConfig()).start();
+
+ assertThat(logTester.logs()).hasSize(2);
+ assertThat(logTester.logs(LoggerLevel.WARN)).contains(
+ "The database must be manually upgraded. Please backup the database and browse /setup. "
+ + "For more information: https://docs.sonarqube.org/latest/setup/upgrading",
+ "\n################################################################################\n" +
+ " The database must be manually upgraded. Please backup the database and browse /setup. "
+ + "For more information: https://docs.sonarqube.org/latest/setup/upgrading\n" +
+ "################################################################################");
+ }
+
+ @Test
+ public void do_nothing_if_up_to_date() {
+ DatabaseVersion version = mock(DatabaseVersion.class);
+ when(version.getStatus()).thenReturn(DatabaseVersion.Status.UP_TO_DATE);
+ new DatabaseServerCompatibility(version, settings.asConfig()).start();
+ // no error
+ }
+
+ @Test
+ public void upgrade_automatically_if_blue_green_deployment() {
+ settings.setProperty("sonar.blueGreenEnabled", "true");
+ DatabaseVersion version = mock(DatabaseVersion.class);
+ when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);
+ when(version.getVersion()).thenReturn(Optional.of(DatabaseVersion.MIN_UPGRADE_VERSION));
+
+ new DatabaseServerCompatibility(version, settings.asConfig()).start();
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DefaultServerUpgradeStatusTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DefaultServerUpgradeStatusTest.java
new file mode 100644
index 00000000000..050fc56cbac
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DefaultServerUpgradeStatusTest.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.platform;
+
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.config.internal.ConfigurationBridge;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.server.platform.db.migration.step.MigrationSteps;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultServerUpgradeStatusTest {
+ private static final long LAST_VERSION = 150;
+ private MigrationSteps migrationSteps = mock(MigrationSteps.class);
+ private DatabaseVersion dbVersion = mock(DatabaseVersion.class);
+ private MapSettings settings = new MapSettings();
+ private DefaultServerUpgradeStatus underTest = new DefaultServerUpgradeStatus(dbVersion, migrationSteps, new ConfigurationBridge(settings));
+
+ @Before
+ public void setUp() throws Exception {
+ when(migrationSteps.getMaxMigrationNumber()).thenReturn(LAST_VERSION);
+ }
+
+ @Test
+ public void shouldBeFreshInstallation() {
+ when(migrationSteps.getMaxMigrationNumber()).thenReturn(150L);
+ when(dbVersion.getVersion()).thenReturn(Optional.empty());
+
+ underTest.start();
+
+ assertThat(underTest.isFreshInstall()).isTrue();
+ assertThat(underTest.isUpgraded()).isFalse();
+ assertThat(underTest.getInitialDbVersion()).isEqualTo(-1);
+ }
+
+ @Test
+ public void shouldBeUpgraded() {
+ when(dbVersion.getVersion()).thenReturn(Optional.of(50L));
+
+ underTest.start();
+
+ assertThat(underTest.isFreshInstall()).isFalse();
+ assertThat(underTest.isUpgraded()).isTrue();
+ assertThat(underTest.getInitialDbVersion()).isEqualTo(50);
+ }
+
+ @Test
+ public void shouldNotBeUpgraded() {
+ when(dbVersion.getVersion()).thenReturn(Optional.of(LAST_VERSION));
+
+ underTest.start();
+
+ assertThat(underTest.isFreshInstall()).isFalse();
+ assertThat(underTest.isUpgraded()).isFalse();
+ assertThat(underTest.getInitialDbVersion()).isEqualTo((int) LAST_VERSION);
+ }
+
+ @Test
+ public void isBlueGreen() {
+ settings.clear();
+ assertThat(underTest.isBlueGreen()).isFalse();
+
+ settings.setProperty("sonar.blueGreenEnabled", true);
+ assertThat(underTest.isBlueGreen()).isTrue();
+
+ settings.setProperty("sonar.blueGreenEnabled", false);
+ assertThat(underTest.isBlueGreen()).isFalse();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/PersistentSettingsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/PersistentSettingsTest.java
new file mode 100644
index 00000000000..884c7d343de
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/PersistentSettingsTest.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.platform;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.server.setting.SettingsChangeNotifier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class PersistentSettingsTest {
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+ private Settings delegate = new MapSettings();
+ private SettingsChangeNotifier changeNotifier = mock(SettingsChangeNotifier.class);
+ private PersistentSettings underTest = new PersistentSettings(delegate, dbTester.getDbClient(), changeNotifier);
+
+ @Test
+ public void insert_property_into_database_and_notify_extensions() {
+ assertThat(underTest.getString("foo")).isNull();
+
+ underTest.saveProperty("foo", "bar");
+
+ assertThat(underTest.getString("foo")).isEqualTo("bar");
+ assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty("foo").getValue()).isEqualTo("bar");
+ verify(changeNotifier).onGlobalPropertyChange("foo", "bar");
+ }
+
+ @Test
+ public void delete_property_from_database_and_notify_extensions() {
+ underTest.saveProperty("foo", "bar");
+ underTest.saveProperty("foo", null);
+
+ assertThat(underTest.getString("foo")).isNull();
+ assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty("foo")).isNull();
+ verify(changeNotifier).onGlobalPropertyChange("foo", null);
+ }
+
+ @Test
+ public void getSettings_returns_delegate() {
+ assertThat(underTest.getSettings()).isSameAs(delegate);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StartupMetadataPersisterTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StartupMetadataPersisterTest.java
new file mode 100644
index 00000000000..c4c84ee1b94
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StartupMetadataPersisterTest.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.platform;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.property.PropertyDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class StartupMetadataPersisterTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ private StartupMetadata metadata = new StartupMetadata(123_456_789L);
+ private StartupMetadataPersister underTest = new StartupMetadataPersister(metadata, dbTester.getDbClient());
+
+ @Test
+ public void persist_metadata_at_startup() {
+ underTest.start();
+
+ assertPersistedProperty(CoreProperties.SERVER_STARTTIME, DateUtils.formatDateTime(metadata.getStartedAt()));
+
+ underTest.stop();
+ }
+
+ private void assertPersistedProperty(String propertyKey, String expectedValue) {
+ PropertyDto prop = dbTester.getDbClient().propertiesDao().selectGlobalProperty(dbTester.getSession(), propertyKey);
+ assertThat(prop.getValue()).isEqualTo(expectedValue);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java
new file mode 100644
index 00000000000..b89d6c2739b
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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 java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.batch.ScannerSide;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.extension.CoreExtension;
+import org.sonar.core.extension.CoreExtensionRepository;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.core.extension.CoreExtensionsInstaller.noAdditionalSideFilter;
+import static org.sonar.core.extension.CoreExtensionsInstaller.noExtensionFilter;
+
+public class WebCoreExtensionsInstallerTest {
+ private SonarRuntime sonarRuntime = mock(SonarRuntime.class);
+ private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class);
+
+ private WebCoreExtensionsInstaller underTest = new WebCoreExtensionsInstaller(sonarRuntime, coreExtensionRepository);
+
+ @Test
+ public void install_only_adds_ServerSide_annotated_extension_to_container() {
+ when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of(
+ new CoreExtension() {
+ @Override
+ public String getName() {
+ return "foo";
+ }
+
+ @Override
+ public void load(Context context) {
+ context.addExtensions(CeClass.class, ScannerClass.class, WebServerClass.class,
+ NoAnnotationClass.class, OtherAnnotationClass.class, MultipleAnnotationClass.class);
+ }
+ }));
+ ComponentContainer container = new ComponentContainer();
+
+ underTest.install(container, noExtensionFilter(), noAdditionalSideFilter());
+
+ assertThat(container.getPicoContainer().getComponentAdapters())
+ .hasSize(ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2);
+ assertThat(container.getComponentByType(WebServerClass.class)).isNotNull();
+ assertThat(container.getComponentByType(MultipleAnnotationClass.class)).isNotNull();
+ }
+
+ @ComputeEngineSide
+ public static final class CeClass {
+
+ }
+
+ @ServerSide
+ public static final class WebServerClass {
+
+ }
+
+ @ScannerSide
+ public static final class ScannerClass {
+
+ }
+
+ @ServerSide
+ @ComputeEngineSide
+ @ScannerSide
+ public static final class MultipleAnnotationClass {
+
+ }
+
+ public static final class NoAnnotationClass {
+
+ }
+
+ @DarkSide
+ public static final class OtherAnnotationClass {
+
+ }
+
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface DarkSide {
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartupTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartupTest.java
new file mode 100644
index 00000000000..97ed8c9894a
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartupTest.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.platform.db;
+
+import org.junit.After;
+import org.junit.Test;
+import org.sonar.api.platform.ServerUpgradeStatus;
+import org.sonar.server.platform.db.migration.charset.DatabaseCharsetChecker;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class CheckDatabaseCharsetAtStartupTest {
+
+ private ServerUpgradeStatus upgradeStatus = mock(ServerUpgradeStatus.class);
+ private DatabaseCharsetChecker charsetChecker = mock(DatabaseCharsetChecker.class);
+ private CheckDatabaseCharsetAtStartup underTest = new CheckDatabaseCharsetAtStartup(upgradeStatus, charsetChecker);
+
+ @After
+ public void tearDown() {
+ underTest.stop();
+ }
+
+ @Test
+ public void test_fresh_install() {
+ when(upgradeStatus.isFreshInstall()).thenReturn(true);
+
+ underTest.start();
+
+ verify(charsetChecker).check(DatabaseCharsetChecker.State.FRESH_INSTALL);
+ }
+
+ @Test
+ public void test_upgrade() {
+ when(upgradeStatus.isUpgraded()).thenReturn(true);
+
+ underTest.start();
+
+ verify(charsetChecker).check(DatabaseCharsetChecker.State.UPGRADE);
+ }
+
+ @Test
+ public void test_regular_startup() {
+ when(upgradeStatus.isFreshInstall()).thenReturn(false);
+
+ underTest.start();
+
+ verify(charsetChecker).check(DatabaseCharsetChecker.State.STARTUP);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseFactoryTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseFactoryTest.java
new file mode 100644
index 00000000000..7e30958d539
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseFactoryTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.db;
+
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.System2;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+
+public class EmbeddedDatabaseFactoryTest {
+
+ private MapSettings settings = new MapSettings();
+ private System2 system2 = mock(System2.class);
+
+ @Test
+ public void should_start_and_stop_tcp_h2_database() {
+ settings.setProperty(JDBC_URL.getKey(), "jdbc:h2:tcp:localhost");
+
+ EmbeddedDatabase embeddedDatabase = mock(EmbeddedDatabase.class);
+
+ EmbeddedDatabaseFactory databaseFactory = new EmbeddedDatabaseFactory(settings.asConfig(), system2) {
+ @Override
+ EmbeddedDatabase createEmbeddedDatabase() {
+ return embeddedDatabase;
+ }
+ };
+ databaseFactory.start();
+ databaseFactory.stop();
+
+ verify(embeddedDatabase).start();
+ verify(embeddedDatabase).stop();
+ }
+
+ @Test
+ public void should_not_start_mem_h2_database() {
+ settings.setProperty(JDBC_URL.getKey(), "jdbc:h2:mem");
+
+ EmbeddedDatabase embeddedDatabase = mock(EmbeddedDatabase.class);
+
+ EmbeddedDatabaseFactory databaseFactory = new EmbeddedDatabaseFactory(settings.asConfig(), system2) {
+ @Override
+ EmbeddedDatabase createEmbeddedDatabase() {
+ return embeddedDatabase;
+ }
+ };
+ databaseFactory.start();
+
+ verify(embeddedDatabase, never()).start();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseTest.java
new file mode 100644
index 00000000000..5672b7b60cf
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.db;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.sql.DriverManager;
+import org.h2.Driver;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.process.NetworkUtilsImpl;
+
+import static junit.framework.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.sonar.process.ProcessProperties.Property.JDBC_EMBEDDED_PORT;
+import static org.sonar.process.ProcessProperties.Property.JDBC_PASSWORD;
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+import static org.sonar.process.ProcessProperties.Property.JDBC_USERNAME;
+import static org.sonar.process.ProcessProperties.Property.PATH_DATA;
+
+public class EmbeddedDatabaseTest {
+
+ private static final String LOOPBACK_ADDRESS = InetAddress.getLoopbackAddress().getHostAddress();
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public LogTester logTester = new LogTester();
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+ @Rule
+ public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+
+ private MapSettings settings = new MapSettings();
+ private System2 system2 = mock(System2.class);
+ private EmbeddedDatabase underTest = new EmbeddedDatabase(settings.asConfig(), system2);
+
+ @After
+ public void tearDown() {
+ if (underTest != null) {
+ underTest.stop();
+ }
+ }
+
+ @Test
+ public void start_fails_with_IAE_if_property_Data_Path_is_not_set() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Missing property " + PATH_DATA.getKey());
+
+ underTest.start();
+ }
+
+ @Test
+ public void start_fails_with_IAE_if_property_Data_Path_is_empty() {
+ settings.setProperty(PATH_DATA.getKey(), "");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Missing property " + PATH_DATA.getKey());
+
+ underTest.start();
+ }
+
+ @Test
+ public void start_fails_with_IAE_if_JDBC_URL_settings_is_not_set() throws IOException {
+ settings.setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Missing property " + JDBC_URL.getKey());
+
+ underTest.start();
+ }
+
+ @Test
+ public void start_fails_with_IAE_if_embedded_port_settings_is_not_set() throws IOException {
+ settings
+ .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath())
+ .setProperty(JDBC_URL.getKey(), "jdbc url");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Missing property " + JDBC_EMBEDDED_PORT.getKey());
+
+ underTest.start();
+ }
+
+ @Test
+ public void start_ignores_URL_to_create_database_and_uses_empty_username_and_password_when_then_are_not_set() throws IOException {
+ int port = NetworkUtilsImpl.INSTANCE.getNextAvailablePort(InetAddress.getLoopbackAddress());
+ settings
+ .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath())
+ .setProperty(JDBC_URL.getKey(), "jdbc url")
+ .setProperty(JDBC_EMBEDDED_PORT.getKey(), "" + port);
+
+ underTest.start();
+
+ checkDbIsUp(port, "", "");
+ }
+
+ @Test
+ public void start_creates_db_and_adds_tcp_listener() throws IOException {
+ int port = NetworkUtilsImpl.INSTANCE.getNextAvailablePort(InetAddress.getLoopbackAddress());
+ settings
+ .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath())
+ .setProperty(JDBC_URL.getKey(), "jdbc url")
+ .setProperty(JDBC_EMBEDDED_PORT.getKey(), "" + port)
+ .setProperty(JDBC_USERNAME.getKey(), "foo")
+ .setProperty(JDBC_PASSWORD.getKey(), "bar");
+
+ underTest.start();
+
+ checkDbIsUp(port, "foo", "bar");
+
+ // H2 listens on loopback address only
+ verify(system2).setProperty("h2.bindAddress", LOOPBACK_ADDRESS);
+ }
+
+ @Test
+ public void start_supports_in_memory_H2_JDBC_URL() throws IOException {
+ int port = NetworkUtilsImpl.INSTANCE.getNextAvailablePort(InetAddress.getLoopbackAddress());
+ settings
+ .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath())
+ .setProperty(JDBC_URL.getKey(), "jdbc:h2:mem:sonar")
+ .setProperty(JDBC_EMBEDDED_PORT.getKey(), "" + port)
+ .setProperty(JDBC_USERNAME.getKey(), "foo")
+ .setProperty(JDBC_PASSWORD.getKey(), "bar");
+
+ underTest.start();
+
+ checkDbIsUp(port, "foo", "bar");
+ }
+
+ private void checkDbIsUp(int port, String user, String password) {
+ try {
+ String driverUrl = String.format("jdbc:h2:tcp://%s:%d/sonar;USER=%s;PASSWORD=%s", LOOPBACK_ADDRESS, port, user, password);
+ DriverManager.registerDriver(new Driver());
+ DriverManager.getConnection(driverUrl).close();
+ } catch (Exception ex) {
+ fail("Unable to connect after start");
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java
new file mode 100644
index 00000000000..1c8d50cd7f5
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.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.platform.db.migration;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.DbClient;
+import org.sonar.db.dialect.Dialect;
+import org.sonar.db.dialect.H2;
+import org.sonar.db.dialect.MsSql;
+import org.sonar.db.dialect.Oracle;
+import org.sonar.db.dialect.PostgreSql;
+import org.sonar.server.platform.DefaultServerUpgradeStatus;
+import org.sonar.server.platform.db.migration.engine.MigrationEngine;
+
+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.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class AutoDbMigrationTest {
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS);
+ private DefaultServerUpgradeStatus serverUpgradeStatus = mock(DefaultServerUpgradeStatus.class);
+ private MigrationEngine migrationEngine = mock(MigrationEngine.class);
+ private AutoDbMigration underTest = new AutoDbMigration(serverUpgradeStatus, migrationEngine);
+
+ @Test
+ public void start_runs_MigrationEngine_on_h2_if_fresh_install() {
+ start_runs_MigrationEngine_for_dialect_if_fresh_install(new H2());
+ }
+
+ @Test
+ public void start_runs_MigrationEngine_on_postgre_if_fresh_install() {
+ start_runs_MigrationEngine_for_dialect_if_fresh_install(new PostgreSql());
+ }
+
+ @Test
+ public void start_runs_MigrationEngine_on_Oracle_if_fresh_install() {
+ start_runs_MigrationEngine_for_dialect_if_fresh_install(new Oracle());
+ }
+
+ @Test
+ public void start_runs_MigrationEngine_on_MsSQL_if_fresh_install() {
+ start_runs_MigrationEngine_for_dialect_if_fresh_install(new MsSql());
+ }
+
+ private void start_runs_MigrationEngine_for_dialect_if_fresh_install(Dialect dialect) {
+ mockDialect(dialect);
+ mockFreshInstall(true);
+
+ underTest.start();
+
+ verify(migrationEngine).execute();
+ verifyInfoLog();
+ }
+
+ @Test
+ public void start_does_nothing_if_not_fresh_install() {
+ mockFreshInstall(false);
+
+ underTest.start();
+
+ verifyZeroInteractions(migrationEngine);
+ assertThat(logTester.logs(LoggerLevel.INFO)).isEmpty();
+ }
+
+ @Test
+ public void start_runs_MigrationEngine_if_blue_green_upgrade() {
+ mockFreshInstall(false);
+ when(serverUpgradeStatus.isUpgraded()).thenReturn(true);
+ when(serverUpgradeStatus.isBlueGreen()).thenReturn(true);
+
+ underTest.start();
+
+ verify(migrationEngine).execute();
+ assertThat(logTester.logs(LoggerLevel.INFO)).contains("Automatically perform DB migration on blue/green deployment");
+ }
+
+ @Test
+ public void start_does_nothing_if_blue_green_but_no_upgrade() {
+ mockFreshInstall(false);
+ when(serverUpgradeStatus.isUpgraded()).thenReturn(false);
+ when(serverUpgradeStatus.isBlueGreen()).thenReturn(true);
+
+ underTest.start();
+
+ verifyZeroInteractions(migrationEngine);
+ assertThat(logTester.logs(LoggerLevel.INFO)).isEmpty();
+ }
+
+ @Test
+ public void stop_has_no_effect() {
+ underTest.stop();
+ }
+
+ private void mockFreshInstall(boolean value) {
+ when(serverUpgradeStatus.isFreshInstall()).thenReturn(value);
+ }
+
+ private void mockDialect(Dialect dialect) {
+ when(dbClient.getDatabase().getDialect()).thenReturn(dialect);
+ }
+
+ private void verifyInfoLog() {
+ assertThat(logTester.logs()).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly("Automatically perform DB migration on fresh install");
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceAdaptor.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceAdaptor.java
new file mode 100644
index 00000000000..736d056c22b
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceAdaptor.java
@@ -0,0 +1,100 @@
+/*
+ * 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.db.migration;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Adaptor for the DatabaseMigrationExecutorService interface which implementation of methods all throw
+ * UnsupportedOperationException.
+ */
+class DatabaseMigrationExecutorServiceAdaptor implements DatabaseMigrationExecutorService {
+
+ @Override
+ public void execute(Runnable command) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void shutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> Future<T> submit(Callable<T> task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> Future<T> submit(Runnable task, T result) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Future<?> submit(Runnable task) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplAsynchronousTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplAsynchronousTest.java
new file mode 100644
index 00000000000..693a4468fb7
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplAsynchronousTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.db.migration;
+
+import org.junit.Test;
+import org.sonar.server.platform.Platform;
+import org.sonar.server.platform.db.migration.engine.MigrationEngine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class DatabaseMigrationImplAsynchronousTest {
+
+ private boolean taskSuppliedForAsyncProcess = false;
+ /**
+ * Implementation of execute wraps specified Runnable to add a delay of 200 ms before passing it
+ * to a SingleThread executor to execute asynchronously.
+ */
+ private DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() {
+ @Override
+ public void execute(final Runnable command) {
+ taskSuppliedForAsyncProcess = true;
+ }
+ };
+ private MutableDatabaseMigrationState migrationState = mock(MutableDatabaseMigrationState.class);
+ private Platform platform = mock(Platform.class);
+ private MigrationEngine migrationEngine = mock(MigrationEngine.class);
+ private DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, migrationEngine, platform);
+
+ @Test
+ public void testName() {
+ underTest.startIt();
+
+ assertThat(taskSuppliedForAsyncProcess).isTrue();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java
new file mode 100644
index 00000000000..dd9be0581f8
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.db.migration;
+
+import com.google.common.base.Throwables;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.After;
+import org.junit.Test;
+import org.sonar.server.platform.Platform;
+import org.sonar.server.platform.db.migration.engine.MigrationEngine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class DatabaseMigrationImplConcurrentAccessTest {
+
+ private ExecutorService pool = Executors.newFixedThreadPool(2);
+ /**
+ * Latch is used to make sure both testing threads try and call {@link DatabaseMigrationImpl#startIt()} at the
+ * same time
+ */
+ private CountDownLatch latch = new CountDownLatch(2);
+
+ /**
+ * Implementation of execute runs Runnable synchronously
+ */
+ private DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ };
+ private AtomicInteger triggerCount = new AtomicInteger();
+ private MigrationEngine incrementingMigrationEngine = new MigrationEngine() {
+ @Override
+ public void execute() {
+ // need execute to consume some time to avoid UT to fail because it ran too fast and threads never executed concurrently
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {
+ Throwables.propagate(e);
+ }
+ triggerCount.incrementAndGet();
+ }
+ };
+ private MutableDatabaseMigrationState migrationState = mock(MutableDatabaseMigrationState.class);
+ private Platform platform = mock(Platform.class);
+ private DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, incrementingMigrationEngine, platform);
+
+ @After
+ public void tearDown() {
+ pool.shutdownNow();
+ }
+
+ @Test
+ public void two_concurrent_calls_to_startit_call_migration_engine_only_once() throws Exception {
+ pool.submit(new CallStartit());
+ pool.submit(new CallStartit());
+
+ pool.awaitTermination(2, TimeUnit.SECONDS);
+
+ assertThat(triggerCount.get()).isEqualTo(1);
+ }
+
+ private class CallStartit implements Runnable {
+ @Override
+ public void run() {
+ latch.countDown();
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ // propagate interruption
+ Thread.currentThread().interrupt();
+ }
+ underTest.startIt();
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java
new file mode 100644
index 00000000000..d4357906db3
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.db.migration;
+
+import java.util.Date;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.sonar.server.platform.Platform;
+import org.sonar.server.platform.db.migration.engine.MigrationEngine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Unit test for DatabaseMigrationImpl which does not test any of its concurrency management and asynchronous execution code.
+ */
+public class DatabaseMigrationImplTest {
+ private static final Throwable AN_ERROR = new RuntimeException("runtime exception created on purpose");
+
+ /**
+ * Implementation of execute runs Runnable synchronously.
+ */
+ private DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ };
+ private MutableDatabaseMigrationState migrationState = new DatabaseMigrationStateImpl();
+ private Platform platform = mock(Platform.class);
+ private MigrationEngine migrationEngine = mock(MigrationEngine.class);
+ private InOrder inOrder = inOrder(platform, migrationEngine);
+
+ private DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, migrationEngine, platform);
+
+ @Test
+ public void startit_calls_MigrationEngine_execute() {
+ underTest.startIt();
+
+ inOrder.verify(migrationEngine).execute();
+ inOrder.verify(platform).doStart();
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void status_is_SUCCEEDED_and_failure_is_null_when_trigger_runs_without_an_exception() {
+ underTest.startIt();
+
+ assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.SUCCEEDED);
+ assertThat(migrationState.getError()).isNull();
+ assertThat(migrationState.getStartedAt()).isNotNull();
+ }
+
+ @Test
+ public void status_is_FAILED_and_failure_stores_the_exception_when_trigger_throws_an_exception() {
+ mockMigrationThrowsError();
+
+ underTest.startIt();
+
+ assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.FAILED);
+ assertThat(migrationState.getError()).isSameAs(AN_ERROR);
+ assertThat(migrationState.getStartedAt()).isNotNull();
+ }
+
+ @Test
+ public void successive_calls_to_startIt_reset_status_startedAt_and_failureError() {
+ mockMigrationThrowsError();
+
+ underTest.startIt();
+
+ assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.FAILED);
+ assertThat(migrationState.getError()).isSameAs(AN_ERROR);
+ Date firstStartDate = migrationState.getStartedAt();
+ assertThat(firstStartDate).isNotNull();
+
+ mockMigrationDoesNothing();
+
+ underTest.startIt();
+
+ assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.SUCCEEDED);
+ assertThat(migrationState.getError()).isNull();
+ assertThat(migrationState.getStartedAt()).isNotSameAs(firstStartDate);
+ }
+
+ private void mockMigrationThrowsError() {
+ doThrow(AN_ERROR).when(migrationEngine).execute();
+ }
+
+ private void mockMigrationDoesNothing() {
+ doNothing().when(migrationEngine).execute();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/BaseSectionMBeanTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/BaseSectionMBeanTest.java
new file mode 100644
index 00000000000..7cd99dedd22
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/BaseSectionMBeanTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.monitoring;
+
+import java.lang.management.ManagementFactory;
+import javax.annotation.CheckForNull;
+import javax.management.InstanceNotFoundException;
+import javax.management.ObjectInstance;
+import javax.management.ObjectName;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BaseSectionMBeanTest {
+
+ private FakeSection underTest = new FakeSection();
+
+ @Test
+ public void test_registration() throws Exception {
+ assertThat(getMBean()).isNull();
+
+ underTest.start();
+ assertThat(getMBean()).isNotNull();
+
+ underTest.stop();
+ assertThat(getMBean()).isNull();
+ }
+
+ @Test
+ public void do_not_fail_when_stopping_unstarted() throws Exception {
+ underTest.stop();
+ assertThat(getMBean()).isNull();
+ }
+
+ @CheckForNull
+ private ObjectInstance getMBean() throws Exception {
+ try {
+ return ManagementFactory.getPlatformMBeanServer().getObjectInstance(new ObjectName(underTest.objectName()));
+ } catch (InstanceNotFoundException e) {
+ return null;
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/DbConnectionSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/DbConnectionSectionTest.java
new file mode 100644
index 00000000000..e68ca345091
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/DbConnectionSectionTest.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.platform.monitoring;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.platform.db.migration.version.DatabaseVersion;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+
+public class DbConnectionSectionTest {
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ private DatabaseVersion databaseVersion = mock(DatabaseVersion.class);
+ private SonarRuntime runtime = mock(SonarRuntime.class);
+ private DbConnectionSection underTest = new DbConnectionSection(databaseVersion, dbTester.getDbClient(), runtime);
+
+ @Test
+ public void jmx_name_is_not_empty() {
+ assertThat(underTest.name()).isEqualTo("Database");
+ }
+
+ @Test
+ public void pool_info() {
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThat(attribute(section, "Pool Max Connections").getLongValue()).isGreaterThan(0L);
+ assertThat(attribute(section, "Pool Idle Connections").getLongValue()).isGreaterThanOrEqualTo(0L);
+ assertThat(attribute(section, "Pool Min Idle Connections").getLongValue()).isGreaterThanOrEqualTo(0L);
+ assertThat(attribute(section, "Pool Max Idle Connections").getLongValue()).isGreaterThanOrEqualTo(0L);
+ assertThat(attribute(section, "Pool Max Wait (ms)")).isNotNull();
+ assertThat(attribute(section, "Pool Remove Abandoned")).isNotNull();
+ assertThat(attribute(section, "Pool Remove Abandoned Timeout (seconds)").getLongValue()).isGreaterThanOrEqualTo(0L);
+ }
+
+ @Test
+ public void section_name_depends_on_runtime_side() {
+ when(runtime.getSonarQubeSide()).thenReturn(SonarQubeSide.COMPUTE_ENGINE);
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("Compute Engine Database Connection");
+
+ when(runtime.getSonarQubeSide()).thenReturn(SonarQubeSide.SERVER );
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("Web Database Connection");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsIndexesSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsIndexesSectionTest.java
new file mode 100644
index 00000000000..d82b90a7319
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsIndexesSectionTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.monitoring;
+
+import org.elasticsearch.ElasticsearchException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsClient;
+import org.sonar.server.es.EsTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class EsIndexesSectionTest {
+
+ @Rule
+ public EsTester es = EsTester.create();
+
+ private EsIndexesSection underTest = new EsIndexesSection(es.client());
+
+ @Test
+ public void name() {
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("Search Indexes");
+ }
+
+ @Test
+ public void index_attributes() {
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ // one index "issues"
+ assertThat(attribute(section, "Index issues - Docs").getLongValue()).isEqualTo(0L);
+ assertThat(attribute(section, "Index issues - Shards").getLongValue()).isGreaterThan(0);
+ assertThat(attribute(section, "Index issues - Store Size").getStringValue()).isNotNull();
+ }
+
+ @Test
+ public void attributes_displays_exception_message_when_cause_null_when_client_fails() {
+ EsClient esClientMock = mock(EsClient.class);
+ EsIndexesSection underTest = new EsIndexesSection(esClientMock);
+ when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with no cause"));
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThatAttributeIs(section, "Error", "RuntimeException with no cause");
+ }
+
+ @Test
+ public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() {
+ EsClient esClientMock = mock(EsClient.class);
+ EsIndexesSection underTest = new EsIndexesSection(esClientMock);
+ when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message")));
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThatAttributeIs(section, "Error", "RuntimeException with cause not ES");
+ }
+
+ @Test
+ public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() {
+ EsClient esClientMock = mock(EsClient.class);
+ EsIndexesSection underTest = new EsIndexesSection(esClientMock);
+ when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message")));
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThatAttributeIs(section, "Error", "some cause message");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java
new file mode 100644
index 00000000000..3a03fc5a696
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.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.platform.monitoring;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsClient;
+import org.sonar.server.es.EsTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class EsStateSectionTest {
+
+ @Rule
+ public EsTester es = EsTester.create();
+
+ private EsStateSection underTest = new EsStateSection(es.client());
+
+ @Test
+ public void name() {
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("Search State");
+ }
+
+ @Test
+ public void es_state() {
+ assertThatAttributeIs(underTest.toProtobuf(), "State", ClusterHealthStatus.GREEN.name());
+ }
+
+ @Test
+ public void node_attributes() {
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThat(attribute(section, "CPU Usage (%)")).isNotNull();
+ assertThat(attribute(section, "Disk Available")).isNotNull();
+ assertThat(attribute(section, "Store Size")).isNotNull();
+ assertThat(attribute(section, "Translog Size")).isNotNull();
+ }
+
+ @Test
+ public void attributes_displays_exception_message_when_cause_null_when_client_fails() {
+ EsClient esClientMock = mock(EsClient.class);
+ EsStateSection underTest = new EsStateSection(esClientMock);
+ when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with no cause"));
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThatAttributeIs(section, "State", "RuntimeException with no cause");
+ }
+
+ @Test
+ public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() {
+ EsClient esClientMock = mock(EsClient.class);
+ EsStateSection underTest = new EsStateSection(esClientMock);
+ when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message")));
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThatAttributeIs(section, "State", "RuntimeException with cause not ES");
+ }
+
+ @Test
+ public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() {
+ EsClient esClientMock = mock(EsClient.class);
+ EsStateSection underTest = new EsStateSection(esClientMock);
+ when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message")));
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThatAttributeIs(section, "State", "some cause message");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSection.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSection.java
new file mode 100644
index 00000000000..bf3c6e01ed0
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSection.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.platform.monitoring;
+
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+public class FakeSection extends BaseSectionMBean implements FakeSectionMBean {
+
+ @Override
+ public int getFake() {
+ return 42;
+ }
+
+ @Override
+ public String name() {
+ return "fake";
+ }
+
+ @Override
+ public ProtobufSystemInfo.Section toProtobuf() {
+ return ProtobufSystemInfo.Section.newBuilder().build();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSectionMBean.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSectionMBean.java
new file mode 100644
index 00000000000..5879452629e
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSectionMBean.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.platform.monitoring;
+
+public interface FakeSectionMBean {
+ int getFake();
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/PluginsSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/PluginsSectionTest.java
new file mode 100644
index 00000000000..5b6837473e4
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/PluginsSectionTest.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.platform.monitoring;
+
+import java.util.Arrays;
+import org.junit.Test;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginRepository;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.updatecenter.common.Version;
+
+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.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class PluginsSectionTest {
+
+ private PluginRepository repo = mock(PluginRepository.class);
+ private PluginsSection underTest = new PluginsSection(repo);
+
+ @Test
+ public void name() {
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("Plugins");
+ }
+
+ @Test
+ public void plugin_name_and_version() {
+ when(repo.getPluginInfos()).thenReturn(Arrays.asList(
+ new PluginInfo("key-1")
+ .setName("Plugin 1")
+ .setVersion(Version.create("1.1")),
+ new PluginInfo("key-2")
+ .setName("Plugin 2")
+ .setVersion(Version.create("2.2")),
+ new PluginInfo("no-version")
+ .setName("No Version")));
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "key-1", "1.1 [Plugin 1]");
+ assertThatAttributeIs(section, "key-2", "2.2 [Plugin 2]");
+ assertThatAttributeIs(section, "no-version", "[No Version]");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/SettingsSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/SettingsSectionTest.java
new file mode 100644
index 00000000000..5860ed73671
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/SettingsSectionTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.monitoring;
+
+import org.junit.Test;
+import org.sonar.api.PropertyType;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+import static org.apache.commons.lang.StringUtils.repeat;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class SettingsSectionTest {
+
+ private static final String PASSWORD_PROPERTY = "sonar.password";
+
+ private PropertyDefinitions defs = new PropertyDefinitions(PropertyDefinition.builder(PASSWORD_PROPERTY).type(PropertyType.PASSWORD).build());
+ private Settings settings = new MapSettings(defs);
+ private SettingsSection underTest = new SettingsSection(settings);
+
+ @Test
+ public void return_properties_and_sort_by_key() {
+ settings.setProperty("foo", "foo value");
+ settings.setProperty("bar", "bar value");
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "bar", "bar value");
+ assertThatAttributeIs(protobuf, "foo", "foo value");
+
+ // keys are ordered alphabetically
+ assertThat(protobuf.getAttributesList())
+ .extracting(ProtobufSystemInfo.Attribute::getKey)
+ .containsExactly("bar", "foo");
+ }
+
+ @Test
+ public void truncate_long_property_values() {
+ settings.setProperty("foo", repeat("abcde", 1_000));
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ String value = attribute(protobuf, "foo").getStringValue();
+ assertThat(value).hasSize(500).startsWith("abcde");
+ }
+
+ @Test
+ public void value_is_obfuscated_if_key_matches_patterns() {
+ verifyObfuscated(PASSWORD_PROPERTY);
+ verifyObfuscated("foo.password.something");
+ // case insensitive search of "password" term
+ verifyObfuscated("bar.CheckPassword");
+ verifyObfuscated("foo.passcode.something");
+ // case insensitive search of "passcode" term
+ verifyObfuscated("bar.CheckPassCode");
+ verifyObfuscated("foo.something.secured");
+ verifyObfuscated("bar.something.Secured");
+ verifyObfuscated("sonar.auth.jwtBase64Hs256Secret");
+
+ verifyNotObfuscated("securedStuff");
+ verifyNotObfuscated("foo");
+ }
+
+ private void verifyObfuscated(String key) {
+ settings.setProperty(key, "foo");
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, key, "xxxxxxxx");
+ }
+
+ private void verifyNotObfuscated(String key) {
+ settings.setProperty(key, "foo");
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, key, "foo");
+ }
+
+ @Test
+ public void test_monitor_name() {
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("Settings");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/StandaloneSystemSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/StandaloneSystemSectionTest.java
new file mode 100644
index 00000000000..bc233dab4e9
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/StandaloneSystemSectionTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.monitoring;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.platform.Server;
+import org.sonar.api.security.SecurityRealm;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.authentication.IdentityProviderRepositoryRule;
+import org.sonar.server.authentication.TestIdentityProvider;
+import org.sonar.server.log.ServerLogging;
+import org.sonar.server.platform.OfficialDistribution;
+import org.sonar.server.user.SecurityRealmFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class StandaloneSystemSectionTest {
+
+ private static final String SERVER_ID_PROPERTY = "Server ID";
+ private static final String SERVER_ID_VALIDATED_PROPERTY = "Server ID validated";
+
+ @Rule
+ public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule();
+
+ private MapSettings settings = new MapSettings();
+ private Server server = mock(Server.class);
+ private ServerLogging serverLogging = mock(ServerLogging.class);
+ private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class);
+ private OfficialDistribution officialDistribution = mock(OfficialDistribution.class);
+
+ private StandaloneSystemSection underTest = new StandaloneSystemSection(settings.asConfig(), securityRealmFactory, identityProviderRepository, server,
+ serverLogging, officialDistribution);
+
+ @Before
+ public void setUp() throws Exception {
+ when(serverLogging.getRootLoggerLevel()).thenReturn(LoggerLevel.DEBUG);
+ }
+
+ @Test
+ public void name_is_not_empty() {
+ assertThat(underTest.name()).isNotEmpty();
+ }
+
+ @Test
+ public void test_getServerId() {
+ when(server.getId()).thenReturn("ABC");
+ assertThat(underTest.getServerId()).isEqualTo("ABC");
+ }
+
+ @Test
+ public void official_distribution() {
+ when(officialDistribution.check()).thenReturn(true);
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "Official Distribution", true);
+ }
+
+ @Test
+ public void not_an_official_distribution() {
+ when(officialDistribution.check()).thenReturn(false);
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "Official Distribution", false);
+ }
+
+ @Test
+ public void get_realm() {
+ SecurityRealm realm = mock(SecurityRealm.class);
+ when(realm.getName()).thenReturn("LDAP");
+ when(securityRealmFactory.getRealm()).thenReturn(realm);
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "External User Authentication", "LDAP");
+ }
+
+ @Test
+ public void no_realm() {
+ when(securityRealmFactory.getRealm()).thenReturn(null);
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThat(attribute(protobuf, "External User Authentication")).isNull();
+ }
+
+ @Test
+ public void get_enabled_identity_providers() {
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("github")
+ .setName("GitHub")
+ .setEnabled(true));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("bitbucket")
+ .setName("Bitbucket")
+ .setEnabled(true));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("disabled")
+ .setName("Disabled")
+ .setEnabled(false));
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub");
+ }
+
+ @Test
+ public void get_enabled_identity_providers_allowing_users_to_signup() {
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("github")
+ .setName("GitHub")
+ .setEnabled(true)
+ .setAllowsUsersToSignUp(true));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("bitbucket")
+ .setName("Bitbucket")
+ .setEnabled(true)
+ .setAllowsUsersToSignUp(false));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("disabled")
+ .setName("Disabled")
+ .setEnabled(false)
+ .setAllowsUsersToSignUp(true));
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub");
+ }
+
+ @Test
+ public void return_nb_of_processors() {
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThat(attribute(protobuf, "Processors").getLongValue()).isGreaterThan(0);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImplTest.java
new file mode 100644
index 00000000000..e67e84b37ed
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImplTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.monitoring.cluster;
+
+import com.hazelcast.core.Member;
+import com.hazelcast.core.MemberSelector;
+import com.hazelcast.nio.Address;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Collection;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.process.cluster.hz.DistributedAnswer;
+import org.sonar.process.cluster.hz.DistributedCall;
+import org.sonar.process.cluster.hz.HazelcastMember;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo.Section;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo.SystemInfo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+public class AppNodesInfoLoaderImplTest {
+
+ private static final InetAddress AN_ADDRESS = InetAddress.getLoopbackAddress();
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private HazelcastMember hzMember = mock(HazelcastMember.class);
+ private AppNodesInfoLoaderImpl underTest = new AppNodesInfoLoaderImpl(hzMember);
+
+ @Test
+ public void load_info_from_all_nodes() throws Exception {
+ DistributedAnswer<SystemInfo> answer = new DistributedAnswer<>();
+ answer.setAnswer(newMember("foo"), SystemInfo.newBuilder().addSections(Section.newBuilder().build()).build());
+ answer.setTimedOut(newMember("bar"));
+ answer.setFailed(newMember("baz"), new IOException("BOOM"));
+ when(hzMember.call(any(DistributedCall.class), any(MemberSelector.class), anyLong())).thenReturn(answer);
+
+ Collection<NodeInfo> nodes = underTest.load();
+
+ assertThat(nodes).hasSize(3);
+
+ NodeInfo successfulNodeInfo = findNode(nodes, "foo");
+ assertThat(successfulNodeInfo.getName()).isEqualTo("foo");
+ assertThat(successfulNodeInfo.getHost()).hasValue(AN_ADDRESS.getHostAddress());
+ assertThat(successfulNodeInfo.getErrorMessage()).isEmpty();
+ assertThat(successfulNodeInfo.getSections()).hasSize(1);
+
+ NodeInfo timedOutNodeInfo = findNode(nodes, "bar");
+ assertThat(timedOutNodeInfo.getName()).isEqualTo("bar");
+ assertThat(timedOutNodeInfo.getErrorMessage()).hasValue("Failed to retrieve information on time");
+ assertThat(timedOutNodeInfo.getSections()).isEmpty();
+
+ NodeInfo failedNodeInfo = findNode(nodes, "baz");
+ assertThat(failedNodeInfo.getName()).isEqualTo("baz");
+ assertThat(failedNodeInfo.getErrorMessage()).hasValue("Failed to retrieve information: BOOM");
+ assertThat(failedNodeInfo.getSections()).isEmpty();
+ }
+
+ private NodeInfo findNode(Collection<NodeInfo> nodes, String name) {
+ return nodes.stream()
+ .filter(n -> n.getName().equals(name))
+ .findFirst()
+ .orElseThrow(IllegalStateException::new);
+ }
+
+ private Member newMember(String name) {
+ Member member = mock(Member.class, Mockito.RETURNS_MOCKS);
+ when(member.getStringAttribute(HazelcastMember.Attribute.NODE_NAME.getKey())).thenReturn(name);
+ when(member.getAddress()).thenReturn(new Address(AN_ADDRESS, 6789));
+ return member;
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSectionTest.java
new file mode 100644
index 00000000000..f7d9bf0941f
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSectionTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.monitoring.cluster;
+
+import java.util.Optional;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.ce.configuration.WorkerCountProvider;
+import org.sonar.db.DbClient;
+import org.sonar.db.ce.CeQueueDto;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.property.InternalProperties;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class CeQueueGlobalSectionTest {
+
+ private DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS);
+ private WorkerCountProvider workerCountProvider = mock(WorkerCountProvider.class);
+
+ @Test
+ public void test_queue_state_with_default_settings() {
+ when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.PENDING))).thenReturn(10);
+ when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.IN_PROGRESS))).thenReturn(1);
+
+ CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient);
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "Total Pending", 10);
+ assertThatAttributeIs(section, "Total In Progress", 1);
+ assertThatAttributeIs(section, "Max Workers per Node", 1);
+ }
+
+ @Test
+ public void test_queue_state_with_overridden_settings() {
+ when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.PENDING))).thenReturn(10);
+ when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.IN_PROGRESS))).thenReturn(2);
+ when(workerCountProvider.get()).thenReturn(5);
+
+ CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient, workerCountProvider);
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "Total Pending", 10);
+ assertThatAttributeIs(section, "Total In Progress", 2);
+ assertThatAttributeIs(section, "Max Workers per Node", 5);
+ }
+
+ @Test
+ public void test_workers_not_paused() {
+ CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient, workerCountProvider);
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "Workers Paused", false);
+ }
+
+ @Test
+ public void test_workers_paused() {
+ when(dbClient.internalPropertiesDao().selectByKey(any(), eq(InternalProperties.COMPUTE_ENGINE_PAUSE))).thenReturn(Optional.of("true"));
+
+ CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient, workerCountProvider);
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "Workers Paused", true);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSectionTest.java
new file mode 100644
index 00000000000..b82012392b7
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSectionTest.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.platform.monitoring.cluster;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+
+public class EsClusterStateSectionTest {
+
+ @Rule
+ public EsTester es = EsTester.create();
+
+ private EsClusterStateSection underTest = new EsClusterStateSection(es.client());
+
+ @Test
+ public void test_name() {
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("Search State");
+ }
+
+ @Test
+ public void test_attributes() {
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+ assertThat(attribute(section, "Nodes").getLongValue()).isGreaterThan(0);
+ assertThat(attribute(section, "State").getStringValue()).isIn("RED", "YELLOW", "GREEN");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoaderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoaderTest.java
new file mode 100644
index 00000000000..442a55427ad
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoaderTest.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.platform.monitoring.cluster;
+
+import java.util.List;
+import org.junit.Test;
+import org.sonar.process.systeminfo.SystemInfoSection;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GlobalInfoLoaderTest {
+
+ @Test
+ public void call_only_SystemInfoSection_that_inherit_Global() {
+ // two globals and one standard
+ SystemInfoSection[] sections = new SystemInfoSection[] {
+ new TestGlobalSystemInfoSection("foo"), new TestSystemInfoSection("bar"), new TestGlobalSystemInfoSection("baz")};
+
+ GlobalInfoLoader underTest = new GlobalInfoLoader(sections);
+ List<ProtobufSystemInfo.Section> loadedInfo = underTest.load();
+
+ assertThat(loadedInfo).extracting(ProtobufSystemInfo.Section::getName)
+ .containsExactlyInAnyOrder("foo", "baz");
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSectionTest.java
new file mode 100644
index 00000000000..f12c7461f1e
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSectionTest.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.platform.monitoring.cluster;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.platform.Server;
+import org.sonar.api.security.SecurityRealm;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.authentication.IdentityProviderRepositoryRule;
+import org.sonar.server.authentication.TestIdentityProvider;
+import org.sonar.server.user.SecurityRealmFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class GlobalSystemSectionTest {
+
+ @Rule
+ public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule();
+
+ private MapSettings settings = new MapSettings();
+ private Server server = mock(Server.class);
+ private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class);
+
+ private GlobalSystemSection underTest = new GlobalSystemSection(settings.asConfig(),
+ server, securityRealmFactory, identityProviderRepository);
+
+ @Test
+ public void name_is_not_empty() {
+ assertThat(underTest.toProtobuf().getName()).isEqualTo("System");
+ }
+
+ @Test
+ public void get_realm() {
+ SecurityRealm realm = mock(SecurityRealm.class);
+ when(realm.getName()).thenReturn("LDAP");
+ when(securityRealmFactory.getRealm()).thenReturn(realm);
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "External User Authentication", "LDAP");
+ }
+
+ @Test
+ public void no_realm() {
+ when(securityRealmFactory.getRealm()).thenReturn(null);
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThat(attribute(protobuf, "External User Authentication")).isNull();
+ }
+
+ @Test
+ public void get_enabled_identity_providers() {
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("github")
+ .setName("GitHub")
+ .setEnabled(true));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("bitbucket")
+ .setName("Bitbucket")
+ .setEnabled(true));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("disabled")
+ .setName("Disabled")
+ .setEnabled(false));
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub");
+ }
+
+ @Test
+ public void get_enabled_identity_providers_allowing_users_to_signup() {
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("github")
+ .setName("GitHub")
+ .setEnabled(true)
+ .setAllowsUsersToSignUp(true));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("bitbucket")
+ .setName("Bitbucket")
+ .setEnabled(true)
+ .setAllowsUsersToSignUp(false));
+ identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
+ .setKey("disabled")
+ .setName("Disabled")
+ .setEnabled(false)
+ .setAllowsUsersToSignUp(true));
+
+ ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
+ assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeInfoTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeInfoTest.java
new file mode 100644
index 00000000000..e0863fee41b
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeInfoTest.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.platform.monitoring.cluster;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NodeInfoTest {
+
+ @Test
+ public void test_equals_and_hashCode() {
+ NodeInfo foo = new NodeInfo("foo");
+ NodeInfo bar = new NodeInfo("bar");
+ NodeInfo bar2 = new NodeInfo("bar");
+
+ assertThat(foo.equals(foo)).isTrue();
+ assertThat(foo.equals(bar)).isFalse();
+ assertThat(bar.equals(bar2)).isTrue();
+
+ assertThat(bar.hashCode()).isEqualTo(bar.hashCode());
+ assertThat(bar.hashCode()).isEqualTo(bar2.hashCode());
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSectionTest.java
new file mode 100644
index 00000000000..9fdb245d475
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSectionTest.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.platform.monitoring.cluster;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.platform.Server;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.platform.OfficialDistribution;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.ProcessProperties.Property.PATH_DATA;
+import static org.sonar.process.ProcessProperties.Property.PATH_HOME;
+import static org.sonar.process.ProcessProperties.Property.PATH_LOGS;
+import static org.sonar.process.ProcessProperties.Property.PATH_TEMP;
+import static org.sonar.process.ProcessProperties.Property.PATH_WEB;
+import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
+import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;
+
+public class NodeSystemSectionTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private MapSettings settings = new MapSettings();
+ private Server server = mock(Server.class, RETURNS_DEEP_STUBS);
+ private OfficialDistribution officialDistrib = mock(OfficialDistribution.class);
+ private NodeSystemSection underTest = new NodeSystemSection(settings.asConfig(), server, officialDistrib);
+
+ @Test
+ public void test_section_name() {
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThat(section.getName()).isEqualTo("System");
+ }
+
+ @Test
+ public void return_server_version() {
+ when(server.getVersion()).thenReturn("6.6");
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "Version", "6.6");
+ }
+
+ @Test
+ public void return_official_distribution_flag() {
+ when(officialDistrib.check()).thenReturn(true);
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "Official Distribution", true);
+ }
+
+ @Test
+ public void return_nb_of_processors() {
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThat(attribute(section, "Processors").getLongValue()).isGreaterThan(0);
+ }
+
+ @Test
+ public void return_dir_paths() {
+ settings.setProperty(PATH_HOME.getKey(), "/home");
+ settings.setProperty(PATH_DATA.getKey(), "/data");
+ settings.setProperty(PATH_TEMP.getKey(), "/temp");
+ settings.setProperty(PATH_LOGS.getKey(), "/logs");
+ settings.setProperty(PATH_WEB.getKey(), "/web");
+
+ ProtobufSystemInfo.Section section = underTest.toProtobuf();
+
+ assertThatAttributeIs(section, "Home Dir", "/home");
+ assertThatAttributeIs(section, "Data Dir", "/data");
+ assertThatAttributeIs(section, "Temp Dir", "/temp");
+
+ // logs dir is part of LoggingSection
+ assertThat(attribute(section, "Logs Dir")).isNull();
+
+ // for internal usage
+ assertThat(attribute(section, "Web Dir")).isNull();
+
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImplTest.java
new file mode 100644
index 00000000000..37a712ecc47
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImplTest.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.platform.monitoring.cluster;
+
+import java.util.Collection;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.newindex.FakeIndexDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SearchNodesInfoLoaderImplTest {
+
+ @Rule
+ public EsTester es = EsTester.createCustom(new FakeIndexDefinition());
+
+ private SearchNodesInfoLoaderImpl underTest = new SearchNodesInfoLoaderImpl(es.client());
+
+ @Test
+ public void return_info_from_elasticsearch_api() {
+ Collection<NodeInfo> nodes = underTest.load();
+
+ assertThat(nodes).hasSize(1);
+ NodeInfo node = nodes.iterator().next();
+ assertThat(node.getName()).isNotEmpty();
+ assertThat(node.getHost()).isNotEmpty();
+ assertThat(node.getSections()).hasSize(1);
+ ProtobufSystemInfo.Section stateSection = node.getSections().get(0);
+
+ assertThat(stateSection.getAttributesList())
+ .extracting(ProtobufSystemInfo.Attribute::getKey)
+ .contains(
+ "Disk Available", "Store Size",
+ "JVM Heap Usage", "JVM Heap Used", "JVM Heap Max", "JVM Non Heap Used",
+ "JVM Threads",
+ "Field Data Memory", "Field Data Circuit Breaker Limit", "Field Data Circuit Breaker Estimation",
+ "Request Circuit Breaker Limit", "Request Circuit Breaker Estimation",
+ "Query Cache Memory", "Request Cache Memory");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java
new file mode 100644
index 00000000000..3756ca14b58
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.serverid;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.core.platform.ServerId;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.Uuids;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH;
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+import static org.sonar.server.platform.serverid.ServerIdFactoryImpl.crc32Hex;
+
+@RunWith(DataProviderRunner.class)
+public class ServerIdFactoryImplTest {
+ private static final ServerId A_SERVERID = ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH));
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private MapSettings settings = new MapSettings();
+ private Configuration config = settings.asConfig();
+ private UuidFactory uuidFactory = mock(UuidFactory.class);
+ private JdbcUrlSanitizer jdbcUrlSanitizer = mock(JdbcUrlSanitizer.class);
+ private ServerIdFactoryImpl underTest = new ServerIdFactoryImpl(config, uuidFactory, jdbcUrlSanitizer);
+
+ @Test
+ public void create_from_scratch_fails_with_ISE_if_JDBC_property_not_set() {
+ expectMissingJdbcUrlISE();
+
+ underTest.create();
+ }
+
+ @Test
+ public void create_from_scratch_creates_ServerId_from_JDBC_URL_and_new_uuid() {
+ String jdbcUrl = "jdbc";
+ String uuid = Uuids.create();
+ String sanitizedJdbcUrl = "sanitized_jdbc";
+ settings.setProperty(JDBC_URL.getKey(), jdbcUrl);
+ when(uuidFactory.create()).thenReturn(uuid);
+ when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl);
+
+ ServerId serverId = underTest.create();
+
+ assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl));
+ assertThat(serverId.getDatasetId()).isEqualTo(uuid);
+ }
+
+ @Test
+ public void create_from_ServerId_fails_with_ISE_if_JDBC_property_not_set() {
+ expectMissingJdbcUrlISE();
+
+ underTest.create(A_SERVERID);
+ }
+
+ @Test
+ @UseDataProvider("anyFormatServerId")
+ public void create_from_ServerId_creates_ServerId_from_JDBC_URL_and_serverId_datasetId(ServerId currentServerId) {
+ String jdbcUrl = "jdbc";
+ String sanitizedJdbcUrl = "sanitized_jdbc";
+ settings.setProperty(JDBC_URL.getKey(), jdbcUrl);
+ when(uuidFactory.create()).thenThrow(new IllegalStateException("UuidFactory.create() should not be called"));
+ when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl);
+
+ ServerId serverId = underTest.create(currentServerId);
+
+ assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl));
+ assertThat(serverId.getDatasetId()).isEqualTo(currentServerId.getDatasetId());
+ }
+
+ @DataProvider
+ public static Object[][] anyFormatServerId() {
+ return new Object[][] {
+ {ServerId.parse(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()))},
+ {ServerId.parse(randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))},
+ {ServerId.parse(randomAlphabetic(UUID_DATASET_ID_LENGTH))},
+ {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))},
+ {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH))}
+ };
+ }
+
+ private void expectMissingJdbcUrlISE() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Missing JDBC URL");
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java
new file mode 100644
index 00000000000..0875ad0fcb2
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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.serverid;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import org.sonar.api.SonarEdition;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.internal.SonarRuntimeImpl;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
+import org.sonar.core.platform.ServerId;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.platform.WebServer;
+import org.sonar.server.property.InternalProperties;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.SonarQubeSide.COMPUTE_ENGINE;
+import static org.sonar.api.SonarQubeSide.SERVER;
+import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH;
+
+@RunWith(DataProviderRunner.class)
+public class ServerIdManagerTest {
+
+ private static final ServerId OLD_FORMAT_SERVER_ID = ServerId.parse("20161123150657");
+ private static final ServerId NO_DATABASE_ID_SERVER_ID = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH));
+ private static final ServerId WITH_DATABASE_ID_SERVER_ID = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(NOT_UUID_DATASET_ID_LENGTH));
+ private static final String CHECKSUM_1 = randomAlphanumeric(12);
+
+ @Rule
+ public final DbTester dbTester = DbTester.create(System2.INSTANCE);
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private ServerIdChecksum serverIdChecksum = mock(ServerIdChecksum.class);
+ private ServerIdFactory serverIdFactory = mock(ServerIdFactory.class);
+ private DbClient dbClient = dbTester.getDbClient();
+ private DbSession dbSession = dbTester.getSession();
+ private WebServer webServer = mock(WebServer.class);
+ private ServerIdManager underTest;
+
+ @After
+ public void tearDown() {
+ if (underTest != null) {
+ underTest.stop();
+ }
+ }
+
+ @Test
+ public void web_leader_persists_new_server_id_if_missing() {
+ mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ verifyCreateNewServerIdFromScratch();
+ }
+
+ @Test
+ public void web_leader_persists_new_server_id_if_format_is_old_date() {
+ insertServerId(OLD_FORMAT_SERVER_ID);
+ mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ verifyCreateNewServerIdFromScratch();
+ }
+
+ @Test
+ public void web_leader_persists_new_server_id_if_value_is_empty() {
+ insertServerId("");
+ mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ verifyCreateNewServerIdFromScratch();
+ }
+
+ @Test
+ public void web_leader_keeps_existing_server_id_if_valid() {
+ insertServerId(WITH_DATABASE_ID_SERVER_ID);
+ insertChecksum(CHECKSUM_1);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ }
+
+ @Test
+ public void web_leader_creates_server_id_from_scratch_if_checksum_fails_for_serverId_in_deprecated_format() {
+ ServerId currentServerId = OLD_FORMAT_SERVER_ID;
+ insertServerId(currentServerId);
+ insertChecksum("invalid");
+ mockChecksumOf(currentServerId, "valid");
+ mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ verifyCreateNewServerIdFromScratch();
+ }
+
+ @Test
+ public void web_leader_creates_server_id_from_current_serverId_without_databaseId_if_checksum_fails() {
+ ServerId currentServerId = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH));
+ insertServerId(currentServerId);
+ insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID");
+ mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID");
+ mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ verifyCreateNewServerIdFrom(currentServerId);
+ }
+
+ @Test
+ public void web_leader_creates_server_id_from_current_serverId_with_databaseId_if_checksum_fails() {
+ ServerId currentServerId = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(UUID_DATASET_ID_LENGTH));
+ insertServerId(currentServerId);
+ insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID");
+ mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID");
+ mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ verifyCreateNewServerIdFrom(currentServerId);
+ }
+
+ @Test
+ public void web_leader_generates_missing_checksum_for_current_serverId_with_databaseId() {
+ insertServerId(WITH_DATABASE_ID_SERVER_ID);
+ mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(true);
+
+ test(SERVER);
+
+ verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+ }
+
+ @Test
+ @UseDataProvider("allFormatsOfServerId")
+ public void web_follower_does_not_fail_if_server_id_matches_checksum(ServerId serverId) {
+ insertServerId(serverId);
+ insertChecksum(CHECKSUM_1);
+ mockChecksumOf(serverId, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(false);
+
+ test(SERVER);
+
+ // no changes
+ verifyDb(serverId, CHECKSUM_1);
+ }
+
+ @Test
+ public void web_follower_fails_if_server_id_is_missing() {
+ when(webServer.isStartupLeader()).thenReturn(false);
+
+ expectMissingServerIdException();
+
+ test(SERVER);
+ }
+
+ @Test
+ public void web_follower_fails_if_server_id_is_empty() {
+ insertServerId("");
+ when(webServer.isStartupLeader()).thenReturn(false);
+
+ expectEmptyServerIdException();
+
+ test(SERVER);
+ }
+
+ @Test
+ @UseDataProvider("allFormatsOfServerId")
+ public void web_follower_fails_if_checksum_does_not_match(ServerId serverId) {
+ String dbChecksum = "boom";
+ insertServerId(serverId);
+ insertChecksum(dbChecksum);
+ mockChecksumOf(serverId, CHECKSUM_1);
+ when(webServer.isStartupLeader()).thenReturn(false);
+
+ try {
+ test(SERVER);
+ fail("An ISE should have been raised");
+ }
+ catch (IllegalStateException e) {
+ assertThat(e.getMessage()).isEqualTo("Server ID is invalid");
+ // no changes
+ verifyDb(serverId, dbChecksum);
+ }
+ }
+
+ @Test
+ @UseDataProvider("allFormatsOfServerId")
+ public void compute_engine_does_not_fail_if_server_id_is_valid(ServerId serverId) {
+ insertServerId(serverId);
+ insertChecksum(CHECKSUM_1);
+ mockChecksumOf(serverId, CHECKSUM_1);
+
+ test(COMPUTE_ENGINE);
+
+ // no changes
+ verifyDb(serverId, CHECKSUM_1);
+ }
+
+ @Test
+ public void compute_engine_fails_if_server_id_is_missing() {
+ expectMissingServerIdException();
+
+ test(COMPUTE_ENGINE);
+ }
+
+ @Test
+ public void compute_engine_fails_if_server_id_is_empty() {
+ insertServerId("");
+
+ expectEmptyServerIdException();
+
+ test(COMPUTE_ENGINE);
+ }
+
+ @Test
+ @UseDataProvider("allFormatsOfServerId")
+ public void compute_engine_fails_if_server_id_is_invalid(ServerId serverId) {
+ String dbChecksum = "boom";
+ insertServerId(serverId);
+ insertChecksum(dbChecksum);
+ mockChecksumOf(serverId, CHECKSUM_1);
+
+ try {
+ test(SERVER);
+ fail("An ISE should have been raised");
+ }
+ catch (IllegalStateException e) {
+ assertThat(e.getMessage()).isEqualTo("Server ID is invalid");
+ // no changes
+ verifyDb(serverId, dbChecksum);
+ }
+ }
+
+ @DataProvider
+ public static Object[][] allFormatsOfServerId() {
+ return new Object[][] {
+ {OLD_FORMAT_SERVER_ID},
+ {NO_DATABASE_ID_SERVER_ID},
+ {WITH_DATABASE_ID_SERVER_ID}
+ };
+ }
+
+ private void expectEmptyServerIdException() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Property sonar.core.id is empty in database");
+ }
+
+ private void expectMissingServerIdException() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Property sonar.core.id is missing in database");
+ }
+
+ private void verifyDb(ServerId expectedServerId, String expectedChecksum) {
+ assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID))
+ .extracting(PropertyDto::getValue)
+ .isEqualTo(expectedServerId.toString());
+ assertThat(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.SERVER_ID_CHECKSUM))
+ .hasValue(expectedChecksum);
+ }
+
+ private void mockCreateNewServerId(ServerId newServerId) {
+ when(serverIdFactory.create()).thenReturn(newServerId);
+ when(serverIdFactory.create(any())).thenThrow(new IllegalStateException("new ServerId should not be created from current server id"));
+ }
+
+ private void mockCreateNewServerIdFrom(ServerId currentServerId, ServerId newServerId) {
+ when(serverIdFactory.create()).thenThrow(new IllegalStateException("new ServerId should be created from current server id"));
+ when(serverIdFactory.create(eq(currentServerId))).thenReturn(newServerId);
+ }
+
+ private void verifyCreateNewServerIdFromScratch() {
+ verify(serverIdFactory).create();
+ }
+
+ private void verifyCreateNewServerIdFrom(ServerId currentServerId) {
+ verify(serverIdFactory).create(currentServerId);
+ }
+
+ private void mockChecksumOf(ServerId serverId, String checksum1) {
+ when(serverIdChecksum.computeFor(serverId.toString())).thenReturn(checksum1);
+ }
+
+ private void insertServerId(ServerId serverId) {
+ insertServerId(serverId.toString());
+ }
+
+ private void insertServerId(String serverId) {
+ dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(CoreProperties.SERVER_ID).setValue(serverId.toString()));
+ dbSession.commit();
+ }
+
+ private void insertChecksum(String value) {
+ dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, value);
+ dbSession.commit();
+ }
+
+ private void test(SonarQubeSide side) {
+ underTest = new ServerIdManager(serverIdChecksum, serverIdFactory, dbClient, SonarRuntimeImpl
+ .forSonarQube(Version.create(6, 7), side, SonarEdition.COMMUNITY), webServer);
+ underTest.start();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdModuleTest.java
new file mode 100644
index 00000000000..0fd1a57bcec
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdModuleTest.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.platform.serverid;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class ServerIdModuleTest {
+ private ServerIdModule underTest = new ServerIdModule();
+
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ underTest.configure(container);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 4);
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/HttpRequestIdModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/HttpRequestIdModuleTest.java
new file mode 100644
index 00000000000..6d68d7502b9
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/HttpRequestIdModuleTest.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.platform.web.requestid;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class HttpRequestIdModuleTest {
+ private HttpRequestIdModule underTest = new HttpRequestIdModule();
+
+ @Test
+ public void count_components_in_module() {
+ ComponentContainer container = new ComponentContainer();
+ underTest.configure(container);
+
+ assertThat(container.getPicoContainer().getComponentAdapters())
+ .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 3);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdConfigurationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdConfigurationTest.java
new file mode 100644
index 00000000000..84a62916785
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdConfigurationTest.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.platform.web.requestid;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RequestIdConfigurationTest {
+ private RequestIdConfiguration underTest = new RequestIdConfiguration(50);
+
+ @Test
+ public void getUidGeneratorRenewalCount_returns_value_provided_from_constructor() {
+ assertThat(underTest.getUidGeneratorRenewalCount()).isEqualTo(50);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImplTest.java
new file mode 100644
index 00000000000..5e2bf3c2cf7
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImplTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.web.requestid;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.core.util.UuidGenerator;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class RequestIdGeneratorImplTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private UuidGenerator.WithFixedBase generator1 = increment -> new byte[] {124, 22, 66, 96, 55, 88, 2, 9};
+ private UuidGenerator.WithFixedBase generator2 = increment -> new byte[] {0, 5, 88, 81, 8, 6, 44, 19};
+ private UuidGenerator.WithFixedBase generator3 = increment -> new byte[] {126, 9, 35, 76, 2, 1, 2};
+ private RequestIdGeneratorBase uidGeneratorBase = mock(RequestIdGeneratorBase.class);
+ private IllegalStateException expected = new IllegalStateException("Unexpected third call to createNew");
+
+ @Test
+ public void generate_renews_inner_UuidGenerator_instance_every_number_of_calls_to_generate_specified_in_RequestIdConfiguration_supports_2() {
+ when(uidGeneratorBase.createNew())
+ .thenReturn(generator1)
+ .thenReturn(generator2)
+ .thenReturn(generator3)
+ .thenThrow(expected);
+
+ RequestIdGeneratorImpl underTest = new RequestIdGeneratorImpl(uidGeneratorBase, new RequestIdConfiguration(2));
+
+ assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // using generator1
+ assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // still using generator1
+ assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // renewing generator and using generator2
+ assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // still using generator2
+ assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // renewing generator and using generator3
+ assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // using generator3
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage(expected.getMessage());
+
+ underTest.generate(); // renewing generator and failing
+ }
+
+ @Test
+ public void generate_renews_inner_UuidGenerator_instance_every_number_of_calls_to_generate_specified_in_RequestIdConfiguration_supports_3() {
+ when(uidGeneratorBase.createNew())
+ .thenReturn(generator1)
+ .thenReturn(generator2)
+ .thenReturn(generator3)
+ .thenThrow(expected);
+
+ RequestIdGeneratorImpl underTest = new RequestIdGeneratorImpl(uidGeneratorBase, new RequestIdConfiguration(3));
+
+ assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // using generator1
+ assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // still using generator1
+ assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // still using generator1
+ assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // renewing generator and using it
+ assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // still using generator2
+ assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // still using generator2
+ assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // renewing generator and using it
+ assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // using generator3
+ assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // using generator3
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage(expected.getMessage());
+
+ underTest.generate(); // renewing generator and failing
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/CachingRuleFinderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/CachingRuleFinderTest.java
new file mode 100644
index 00000000000..6c19c1a0ef0
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/CachingRuleFinderTest.java
@@ -0,0 +1,424 @@
+/*
+ * 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.rule;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Consumer;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleQuery;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.rule.RuleDao;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleParamDto;
+import org.sonar.db.rule.RuleTesting;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+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.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class CachingRuleFinderTest {
+ @org.junit.Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ private DbClient dbClient = dbTester.getDbClient();
+
+ private AlwaysIncreasingSystem2 system2 = new AlwaysIncreasingSystem2();
+ private RuleDefinitionDto[] ruleDefinitions;
+ private RuleParamDto[] ruleParams;
+ private CachingRuleFinder underTest;
+
+ @Before()
+ public void setUp() throws Exception {
+ Consumer<RuleDefinitionDto> setUpdatedAt = rule -> rule.setUpdatedAt(system2.now());
+ this.ruleDefinitions = new RuleDefinitionDto[] {
+ dbTester.rules().insert(setUpdatedAt),
+ dbTester.rules().insert(setUpdatedAt),
+ dbTester.rules().insert(setUpdatedAt),
+ dbTester.rules().insert(setUpdatedAt),
+ dbTester.rules().insert(setUpdatedAt),
+ dbTester.rules().insert(setUpdatedAt)
+ };
+ this.ruleParams = Arrays.stream(ruleDefinitions)
+ .map(rule -> dbTester.rules().insertRuleParam(rule))
+ .toArray(RuleParamDto[]::new);
+
+ underTest = new CachingRuleFinder(dbClient);
+
+ // delete all data from DB to ensure tests rely on cache exclusively
+ dbTester.executeUpdateSql("delete from rules");
+ dbTester.executeUpdateSql("delete from rules_parameters");
+ assertThat(dbTester.countRowsOfTable("rules")).isZero();
+ assertThat(dbTester.countRowsOfTable("rules_parameters")).isZero();
+ }
+
+ @Test
+ public void constructor_reads_rules_from_DB() {
+ DbClient dbClient = mock(DbClient.class);
+ DbSession dbSession = mock(DbSession.class);
+ RuleDao ruleDao = mock(RuleDao.class);
+ when(dbClient.openSession(anyBoolean())).thenReturn(dbSession);
+ when(dbClient.ruleDao()).thenReturn(ruleDao);
+
+ new CachingRuleFinder(dbClient);
+
+ verify(dbClient).openSession(anyBoolean());
+ verify(ruleDao).selectAllDefinitions(dbSession);
+ verifyNoMoreInteractions(ruleDao);
+ }
+
+ @Test
+ public void constructor_reads_parameters_from_DB() {
+ DbClient dbClient = mock(DbClient.class);
+ DbSession dbSession = mock(DbSession.class);
+ RuleDao ruleDao = mock(RuleDao.class);
+ when(dbClient.openSession(anyBoolean())).thenReturn(dbSession);
+ when(dbClient.ruleDao()).thenReturn(ruleDao);
+ List<RuleKey> ruleKeys = Arrays.asList(RuleKey.of("A", "B"), RuleKey.of("C", "D"), RuleKey.of("E", "F"));
+ when(ruleDao.selectAllDefinitions(dbSession)).thenReturn(ruleKeys.stream().map(RuleTesting::newRule).collect(toList()));
+
+ new CachingRuleFinder(dbClient);
+
+ verify(ruleDao).selectRuleParamsByRuleKeys(dbSession, ImmutableSet.copyOf(ruleKeys));
+ }
+
+ @Test
+ public void findById_returns_all_loaded_rules_by_id() {
+ for (int i = 0; i < ruleDefinitions.length; i++) {
+ RuleDefinitionDto ruleDefinition = ruleDefinitions[i];
+ RuleParamDto ruleParam = ruleParams[i];
+
+ org.sonar.api.rules.Rule rule = underTest.findById(ruleDefinition.getId());
+ verifyRule(rule, ruleDefinition, ruleParam);
+ }
+ }
+
+ @Test
+ public void findById_returns_null_for_non_existing_id() {
+ assertThat(underTest.findById(new Random().nextInt())).isNull();
+ }
+
+ @Test
+ public void findByKey_returns_all_loaded_rules_by_id() {
+ for (int i = 0; i < ruleDefinitions.length; i++) {
+ RuleDefinitionDto ruleDefinition = ruleDefinitions[i];
+ RuleParamDto ruleParam = ruleParams[i];
+
+ org.sonar.api.rules.Rule rule = underTest.findByKey(ruleDefinition.getKey());
+ verifyRule(rule, ruleDefinition, ruleParam);
+ assertThat(underTest.findByKey(ruleDefinition.getRepositoryKey(), ruleDefinition.getRuleKey()))
+ .isSameAs(rule);
+ }
+ }
+
+ @Test
+ public void findByKey_returns_null_when_RuleKey_is_null() {
+ assertThat(underTest.findByKey(null)).isNull();
+ }
+
+ @Test
+ public void findByKey_returns_null_when_repository_key_is_null() {
+ assertThat(underTest.findByKey(null, randomAlphabetic(2))).isNull();
+ }
+
+ @Test
+ public void findByKey_returns_null_when_key_is_null() {
+ assertThat(underTest.findByKey(randomAlphabetic(2), null)).isNull();
+ }
+
+ @Test
+ public void findByKey_returns_null_when_both_repository_key_and_key_are_null() {
+ assertThat(underTest.findByKey(null, null)).isNull();
+ }
+
+ @Test
+ public void find_returns_null_when_RuleQuery_is_empty() {
+ assertThat(underTest.find(null)).isNull();
+ }
+
+ @Test
+ public void find_returns_most_recent_rule_when_RuleQuery_has_no_non_null_field() {
+ Rule rule = underTest.find(RuleQuery.create());
+
+ assertThat(toRuleKey(rule)).isEqualTo(ruleDefinitions[5].getKey());
+ }
+
+ @Test
+ public void find_searches_by_exact_match_of_repository_key_and_returns_most_recent_rule() {
+ String repoKey = "ABCD";
+ RuleDefinitionDto[] sameRepoKey = {
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now()))
+ };
+ RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()));
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(toRuleKey(underTest.find(RuleQuery.create().withRepositoryKey(repoKey))))
+ .isEqualTo(sameRepoKey[1].getKey());
+ assertThat(toRuleKey(underTest.find(RuleQuery.create().withRepositoryKey(otherRule.getRepositoryKey()))))
+ .isEqualTo(otherRule.getKey());
+ assertThat(underTest.find(RuleQuery.create().withRepositoryKey(repoKey.toLowerCase())))
+ .isNull();
+ assertThat(underTest.find(RuleQuery.create().withRepositoryKey(randomAlphabetic(3))))
+ .isNull();
+ }
+
+ @Test
+ public void find_searches_by_exact_match_of_ruleKey_and_returns_most_recent_rule() {
+ String ruleKey = "ABCD";
+ RuleDefinitionDto[] sameRuleKey = {
+ dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now()))
+ };
+ RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()));
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(toRuleKey(underTest.find(RuleQuery.create().withKey(ruleKey))))
+ .isEqualTo(sameRuleKey[1].getKey());
+ assertThat(toRuleKey(underTest.find(RuleQuery.create().withKey(otherRule.getRuleKey()))))
+ .isEqualTo(otherRule.getKey());
+ assertThat(underTest.find(RuleQuery.create().withKey(ruleKey.toLowerCase())))
+ .isNull();
+ assertThat(underTest.find(RuleQuery.create().withKey(randomAlphabetic(3))))
+ .isNull();
+ }
+
+ @Test
+ public void find_searches_by_exact_match_of_configKey_and_returns_most_recent_rule() {
+ String configKey = "ABCD";
+ RuleDefinitionDto[] sameConfigKey = {
+ dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now()))
+ };
+ RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()));
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(toRuleKey(underTest.find(RuleQuery.create().withConfigKey(configKey))))
+ .isEqualTo(sameConfigKey[1].getKey());
+ assertThat(toRuleKey(underTest.find(RuleQuery.create().withConfigKey(otherRule.getConfigKey()))))
+ .isEqualTo(otherRule.getKey());
+ assertThat(underTest.find(RuleQuery.create().withConfigKey(configKey.toLowerCase())))
+ .isNull();
+ assertThat(underTest.find(RuleQuery.create().withConfigKey(randomAlphabetic(3))))
+ .isNull();
+ }
+
+ @Test
+ public void find_searches_by_exact_match_and_match_on_all_criterias_and_returns_most_recent_match() {
+ String repoKey = "ABCD";
+ String ruleKey = "EFGH";
+ String configKey = "IJKL";
+ RuleDefinitionDto[] rules = {
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()))
+ };
+ RuleQuery allQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey).withConfigKey(configKey);
+ RuleQuery ruleAndConfigKeyQuery = RuleQuery.create().withKey(ruleKey).withConfigKey(configKey);
+ RuleQuery repoAndConfigKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withConfigKey(configKey);
+ RuleQuery repoAndKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey);
+ RuleQuery configKeyQuery = RuleQuery.create().withConfigKey(configKey);
+ RuleQuery ruleKeyQuery = RuleQuery.create().withKey(ruleKey);
+ RuleQuery repoKeyQuery = RuleQuery.create().withRepositoryKey(repoKey);
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(toRuleKey(underTest.find(allQuery))).isEqualTo(rules[0].getKey());
+ assertThat(toRuleKey(underTest.find(ruleAndConfigKeyQuery))).isEqualTo(rules[1].getKey());
+ assertThat(toRuleKey(underTest.find(repoAndConfigKeyQuery))).isEqualTo(rules[2].getKey());
+ assertThat(toRuleKey(underTest.find(repoAndKeyQuery))).isEqualTo(rules[0].getKey());
+ assertThat(toRuleKey(underTest.find(repoKeyQuery))).isEqualTo(rules[2].getKey());
+ assertThat(toRuleKey(underTest.find(ruleKeyQuery))).isEqualTo(rules[1].getKey());
+ assertThat(toRuleKey(underTest.find(configKeyQuery))).isEqualTo(rules[2].getKey());
+ }
+
+ @Test
+ public void findAll_returns_empty_when_RuleQuery_is_empty() {
+ assertThat(underTest.findAll(null)).isEmpty();
+ }
+
+ @Test
+ public void findAll_returns_all_rules_when_RuleQuery_has_no_non_null_field() {
+ assertThat(underTest.findAll(RuleQuery.create()))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsOnly(Arrays.stream(ruleDefinitions).map(RuleDefinitionDto::getKey).toArray(RuleKey[]::new));
+ }
+
+ @Test
+ public void findAll_returns_all_rules_with_exact_same_repository_key_and_order_them_most_recent_first() {
+ String repoKey = "ABCD";
+ RuleDefinitionDto[] sameRepoKey = {
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now()))
+ };
+ RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()));
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(repoKey)))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(sameRepoKey[1].getKey(), sameRepoKey[0].getKey());
+ assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(otherRule.getRepositoryKey())))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(otherRule.getKey());
+ assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(repoKey.toLowerCase())))
+ .isEmpty();
+ assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(randomAlphabetic(3))))
+ .isEmpty();
+ }
+
+ @Test
+ public void findAll_returns_all_rules_with_exact_same_rulekey_and_order_them_most_recent_first() {
+ String ruleKey = "ABCD";
+ RuleDefinitionDto[] sameRuleKey = {
+ dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now()))
+ };
+ RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()));
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(underTest.findAll(RuleQuery.create().withKey(ruleKey)))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(sameRuleKey[1].getKey(), sameRuleKey[0].getKey());
+ assertThat(underTest.findAll(RuleQuery.create().withKey(otherRule.getRuleKey())))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(otherRule.getKey());
+ assertThat(underTest.findAll(RuleQuery.create().withKey(ruleKey.toLowerCase())))
+ .isEmpty();
+ assertThat(underTest.findAll(RuleQuery.create().withKey(randomAlphabetic(3))))
+ .isEmpty();
+ }
+
+ @Test
+ public void findAll_returns_all_rules_with_exact_same_configkey_and_order_them_most_recent_first() {
+ String configKey = "ABCD";
+ RuleDefinitionDto[] sameConfigKey = {
+ dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now()))
+ };
+ RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()));
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(underTest.findAll(RuleQuery.create().withConfigKey(configKey)))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(sameConfigKey[1].getKey(), sameConfigKey[0].getKey());
+ assertThat(underTest.findAll(RuleQuery.create().withConfigKey(otherRule.getConfigKey())))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(otherRule.getKey());
+ assertThat(underTest.findAll(RuleQuery.create().withConfigKey(configKey.toLowerCase())))
+ .isEmpty();
+ assertThat(underTest.findAll(RuleQuery.create().withConfigKey(randomAlphabetic(3))))
+ .isEmpty();
+ }
+
+ @Test
+ public void findAll_returns_all_rules_which_match_exactly_all_criteria_and_order_then_by_most_recent_first() {
+ String repoKey = "ABCD";
+ String ruleKey = "EFGH";
+ String configKey = "IJKL";
+ RuleDefinitionDto[] rules = {
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setConfigKey(configKey).setUpdatedAt(system2.now())),
+ dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now()))
+ };
+ RuleQuery allQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey).withConfigKey(configKey);
+ RuleQuery ruleAndConfigKeyQuery = RuleQuery.create().withKey(ruleKey).withConfigKey(configKey);
+ RuleQuery repoAndConfigKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withConfigKey(configKey);
+ RuleQuery repoAndKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey);
+ RuleQuery configKeyQuery = RuleQuery.create().withConfigKey(configKey);
+ RuleQuery ruleKeyQuery = RuleQuery.create().withKey(ruleKey);
+ RuleQuery repoKeyQuery = RuleQuery.create().withRepositoryKey(repoKey);
+
+ CachingRuleFinder underTest = new CachingRuleFinder(dbClient);
+
+ assertThat(underTest.findAll(allQuery))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(rules[0].getKey());
+ assertThat(underTest.findAll(ruleAndConfigKeyQuery))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(rules[1].getKey(), rules[0].getKey());
+ assertThat(underTest.findAll(repoAndConfigKeyQuery))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(rules[2].getKey(), rules[0].getKey());
+ assertThat(underTest.findAll(repoAndKeyQuery))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(rules[0].getKey());
+ assertThat(underTest.findAll(repoKeyQuery))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(rules[2].getKey(), rules[0].getKey());
+ assertThat(underTest.findAll(ruleKeyQuery))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(rules[1].getKey(), rules[0].getKey());
+ assertThat(underTest.findAll(configKeyQuery))
+ .extracting(CachingRuleFinderTest::toRuleKey)
+ .containsExactly(rules[2].getKey(), rules[1].getKey(), rules[0].getKey());
+ }
+
+ private static RuleKey toRuleKey(Rule rule) {
+ return RuleKey.of(rule.getRepositoryKey(), rule.getKey());
+ }
+
+ private void verifyRule(Rule rule, RuleDefinitionDto ruleDefinition, RuleParamDto ruleParam) {
+ assertThat(rule).isNotNull();
+
+ assertThat(rule.getName()).isEqualTo(ruleDefinition.getName());
+ assertThat(rule.getLanguage()).isEqualTo(ruleDefinition.getLanguage());
+ assertThat(rule.getKey()).isEqualTo(ruleDefinition.getRuleKey());
+ assertThat(rule.getConfigKey()).isEqualTo(ruleDefinition.getConfigKey());
+ assertThat(rule.isTemplate()).isEqualTo(ruleDefinition.isTemplate());
+ assertThat(rule.getCreatedAt().getTime()).isEqualTo(ruleDefinition.getCreatedAt());
+ assertThat(rule.getUpdatedAt().getTime()).isEqualTo(ruleDefinition.getUpdatedAt());
+ assertThat(rule.getRepositoryKey()).isEqualTo(ruleDefinition.getRepositoryKey());
+ assertThat(rule.getSeverity().name()).isEqualTo(ruleDefinition.getSeverityString());
+ assertThat(rule.getSystemTags()).isEqualTo(ruleDefinition.getSystemTags().stream().toArray(String[]::new));
+ assertThat(rule.getTags()).isEmpty();
+ assertThat(rule.getId()).isEqualTo(ruleDefinition.getId());
+ assertThat(rule.getDescription()).isEqualTo(ruleDefinition.getDescription());
+
+ assertThat(rule.getParams()).hasSize(1);
+ org.sonar.api.rules.RuleParam param = rule.getParams().iterator().next();
+ assertThat(param.getRule()).isSameAs(rule);
+ assertThat(param.getKey()).isEqualTo(ruleParam.getName());
+ assertThat(param.getDescription()).isEqualTo(ruleParam.getDescription());
+ assertThat(param.getType()).isEqualTo(ruleParam.getType());
+ assertThat(param.getDefaultValue()).isEqualTo(ruleParam.getDefaultValue());
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoaderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoaderTest.java
new file mode 100644
index 00000000000..ca338616a5d
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoaderTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.rule;
+
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.api.rules.RuleRepository;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.ValidationMessages;
+import org.sonar.core.i18n.RuleI18nManager;
+import org.sonar.api.impl.server.RulesDefinitionContext;
+import org.sonar.server.debt.DebtModelPluginRepository;
+import org.sonar.server.debt.DebtModelXMLExporter;
+import org.sonar.server.debt.DebtRulesXMLImporter;
+import org.sonar.server.plugins.ServerPluginRepository;
+
+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.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DeprecatedRulesDefinitionLoaderTest {
+
+ @Mock
+ RuleI18nManager i18n;
+
+ @Mock
+ DebtModelPluginRepository debtModelRepository;
+
+ @Mock
+ DebtRulesXMLImporter importer;
+
+ @Mock
+ ServerPluginRepository pluginRepository;
+
+ static class CheckstyleRules extends RuleRepository {
+ public CheckstyleRules() {
+ super("checkstyle", "java");
+ setName("Checkstyle");
+ }
+
+ @Override
+ public List<Rule> createRules() {
+ Rule rule = Rule.create("checkstyle", "ConstantName", "Constant Name");
+ rule.setDescription("Checks that constant names conform to the specified format");
+ rule.setConfigKey("Checker/TreeWalker/ConstantName");
+ rule.setSeverity(RulePriority.BLOCKER);
+ rule.setStatus(Rule.STATUS_BETA);
+ rule.setTags(new String[] {"style", "clumsy"});
+ rule.createParameter("format").setDescription("Regular expression").setDefaultValue("A-Z").setType("REGULAR_EXPRESSION");
+ return Arrays.asList(rule);
+ }
+ }
+
+ static class UseBundles extends RuleRepository {
+ public UseBundles() {
+ super("checkstyle", "java");
+ setName("Checkstyle");
+ }
+
+ @Override
+ public List<Rule> createRules() {
+ Rule rule = Rule.create("checkstyle", "ConstantName");
+ rule.createParameter("format");
+ return Arrays.asList(rule);
+ }
+ }
+
+ @Test
+ public void wrap_deprecated_rule_repositories() {
+ RulesDefinition.Context context = new RulesDefinitionContext();
+ CheckstyleRules checkstyleRules = new CheckstyleRules();
+ when(pluginRepository.getPluginKey(checkstyleRules)).thenReturn("unittest");
+ new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {checkstyleRules}).complete(context);
+
+ assertThat(context.repositories()).hasSize(1);
+ RulesDefinition.Repository checkstyle = context.repository("checkstyle");
+ assertThat(checkstyle).isNotNull();
+ assertThat(checkstyle.key()).isEqualTo("checkstyle");
+ assertThat(checkstyle.name()).isEqualTo("Checkstyle");
+ assertThat(checkstyle.language()).isEqualTo("java");
+ assertThat(checkstyle.rules()).hasSize(1);
+ RulesDefinition.Rule rule = checkstyle.rule("ConstantName");
+ assertThat(rule).isNotNull();
+ assertThat(rule.key()).isEqualTo("ConstantName");
+ assertThat(rule.pluginKey()).isEqualTo("unittest");
+ assertThat(rule.name()).isEqualTo("Constant Name");
+ assertThat(rule.htmlDescription()).isEqualTo("Checks that constant names conform to the specified format");
+ assertThat(rule.severity()).isEqualTo(Severity.BLOCKER);
+ assertThat(rule.internalKey()).isEqualTo("Checker/TreeWalker/ConstantName");
+ assertThat(rule.status()).isEqualTo(RuleStatus.BETA);
+ assertThat(rule.tags()).containsOnly("style", "clumsy");
+ assertThat(rule.params()).hasSize(1);
+ RulesDefinition.Param param = rule.param("format");
+ assertThat(param).isNotNull();
+ assertThat(param.key()).isEqualTo("format");
+ assertThat(param.name()).isEqualTo("format");
+ assertThat(param.description()).isEqualTo("Regular expression");
+ assertThat(param.defaultValue()).isEqualTo("A-Z");
+ }
+
+ @Test
+ public void emulate_the_day_deprecated_api_can_be_dropped() {
+ RulesDefinition.Context context = new RulesDefinitionContext();
+
+ // no more RuleRepository !
+ new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository);
+
+ assertThat(context.repositories()).isEmpty();
+ }
+
+ @Test
+ public void use_l10n_bundles() {
+ RulesDefinition.Context context = new RulesDefinitionContext();
+ when(i18n.getName("checkstyle", "ConstantName")).thenReturn("Constant Name");
+ when(i18n.getDescription("checkstyle", "ConstantName")).thenReturn("Checks that constant names conform to the specified format");
+ when(i18n.getParamDescription("checkstyle", "ConstantName", "format")).thenReturn("Regular expression");
+
+ new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {new UseBundles()}).complete(context);
+
+ RulesDefinition.Repository checkstyle = context.repository("checkstyle");
+ RulesDefinition.Rule rule = checkstyle.rule("ConstantName");
+ assertThat(rule.key()).isEqualTo("ConstantName");
+ assertThat(rule.name()).isEqualTo("Constant Name");
+ assertThat(rule.htmlDescription()).isEqualTo("Checks that constant names conform to the specified format");
+ RulesDefinition.Param param = rule.param("format");
+ assertThat(param.key()).isEqualTo("format");
+ assertThat(param.name()).isEqualTo("format");
+ assertThat(param.description()).isEqualTo("Regular expression");
+ }
+
+ @Test
+ public void define_rule_debt() {
+ RulesDefinition.Context context = new RulesDefinitionContext();
+
+ List<DebtModelXMLExporter.RuleDebt> ruleDebts = newArrayList(
+ new DebtModelXMLExporter.RuleDebt()
+ .setRuleKey(RuleKey.of("checkstyle", "ConstantName"))
+ .setFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
+ .setCoefficient("1d")
+ .setOffset("10min"));
+
+ Reader javaModelReader = mock(Reader.class);
+ when(debtModelRepository.createReaderForXMLFile("java")).thenReturn(javaModelReader);
+ when(debtModelRepository.getContributingPluginList()).thenReturn(newArrayList("java"));
+ when(importer.importXML(eq(javaModelReader), any(ValidationMessages.class))).thenReturn(ruleDebts);
+
+ new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {new CheckstyleRules()}).complete(context);
+
+ assertThat(context.repositories()).hasSize(1);
+ RulesDefinition.Repository checkstyle = context.repository("checkstyle");
+ assertThat(checkstyle.rules()).hasSize(1);
+
+ RulesDefinition.Rule rule = checkstyle.rule("ConstantName");
+ assertThat(rule).isNotNull();
+ assertThat(rule.key()).isEqualTo("ConstantName");
+ assertThat(rule.debtRemediationFunction().type()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET);
+ assertThat(rule.debtRemediationFunction().gapMultiplier()).isEqualTo("1d");
+ assertThat(rule.debtRemediationFunction().baseEffort()).isEqualTo("10min");
+ }
+
+ @Test
+ public void fail_on_invalid_rule_debt() {
+ RulesDefinition.Context context = new RulesDefinitionContext();
+
+ List<DebtModelXMLExporter.RuleDebt> ruleDebts = newArrayList(
+ new DebtModelXMLExporter.RuleDebt()
+ .setRuleKey(RuleKey.of("checkstyle", "ConstantName"))
+ .setFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
+ .setCoefficient("1d"));
+
+ Reader javaModelReader = mock(Reader.class);
+ when(debtModelRepository.createReaderForXMLFile("java")).thenReturn(javaModelReader);
+ when(debtModelRepository.getContributingPluginList()).thenReturn(newArrayList("java"));
+ when(importer.importXML(eq(javaModelReader), any(ValidationMessages.class))).thenReturn(ruleDebts);
+
+ try {
+ new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {new CheckstyleRules()}).complete(context);
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ assertThat(context.repositories()).isEmpty();
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDefinitionsLoaderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDefinitionsLoaderTest.java
new file mode 100644
index 00000000000..3cc43fa62d3
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDefinitionsLoaderTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.rule;
+
+import org.junit.Test;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.server.plugins.ServerPluginRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class RuleDefinitionsLoaderTest {
+
+ @Test
+ public void no_definitions() {
+ CommonRuleDefinitions commonRulesDefinitions = mock(CommonRuleDefinitions.class);
+ RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class)).load();
+
+ assertThat(context.repositories()).isEmpty();
+ }
+
+ @Test
+ public void load_definitions() {
+ CommonRuleDefinitions commonRulesDefinitions = mock(CommonRuleDefinitions.class);
+ RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class),
+ new RulesDefinition[] {
+ new FindbugsDefinitions(), new SquidDefinitions()
+ }).load();
+
+ assertThat(context.repositories()).hasSize(2);
+ assertThat(context.repository("findbugs")).isNotNull();
+ assertThat(context.repository("squid")).isNotNull();
+ }
+
+ @Test
+ public void define_common_rules() {
+ CommonRuleDefinitions commonRulesDefinitions = new FakeCommonRuleDefinitions();
+ RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class),
+ new RulesDefinition[] {
+ new SquidDefinitions()
+ }).load();
+
+ assertThat(context.repositories()).extracting("key").containsOnly("squid", "common-java");
+ assertThat(context.repository("common-java").rules()).extracting("key").containsOnly("InsufficientBranchCoverage");
+ }
+
+ /**
+ * "common-rules" are merged into core 5.2. Previously they were embedded by some plugins. Only the core definition
+ * is taken into account. Others are ignored.
+ */
+ @Test
+ public void plugin_common_rules_are_overridden() {
+ CommonRuleDefinitions commonRulesDefinitions = new FakeCommonRuleDefinitions();
+ RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class),
+ new RulesDefinition[] {
+ new PluginCommonRuleDefinitions()
+ }).load();
+
+ assertThat(context.repositories()).extracting("key").containsOnly("common-java");
+ assertThat(context.repository("common-java").rules()).extracting("key").containsOnly("InsufficientBranchCoverage");
+ assertThat(context.repository("common-java").rule("InsufficientBranchCoverage").name()).isEqualTo("The name as defined by core");
+ }
+
+ static class FindbugsDefinitions implements RulesDefinition {
+ @Override
+ public void define(Context context) {
+ NewRepository repo = context.createRepository("findbugs", "java");
+ repo.setName("Findbugs");
+ repo.createRule("ABC")
+ .setName("ABC")
+ .setHtmlDescription("Description of ABC");
+ repo.done();
+ }
+ }
+
+ static class SquidDefinitions implements RulesDefinition {
+ @Override
+ public void define(Context context) {
+ NewRepository repo = context.createRepository("squid", "java");
+ repo.setName("Squid");
+ repo.createRule("DEF")
+ .setName("DEF")
+ .setHtmlDescription("Description of DEF");
+ repo.done();
+ }
+ }
+
+ static class PluginCommonRuleDefinitions implements RulesDefinition {
+ @Override
+ public void define(RulesDefinition.Context context) {
+ RulesDefinition.NewRepository repo = context.createRepository("common-java", "java");
+ repo.createRule("InsufficientBranchCoverage")
+ .setName("The name as defined by plugin")
+ .setHtmlDescription("The description as defined by plugin");
+ repo.done();
+ }
+ }
+
+ static class FakeCommonRuleDefinitions implements CommonRuleDefinitions {
+ @Override
+ public void define(RulesDefinition.Context context) {
+ RulesDefinition.NewRepository repo = context.createRepository("common-java", "java");
+ repo.createRule("InsufficientBranchCoverage")
+ .setName("The name as defined by core")
+ .setHtmlDescription("The description as defined by core");
+ repo.done();
+ }
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java
new file mode 100644
index 00000000000..4b4b286a962
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.rule;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.assertj.core.groups.Tuple;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.rule.DeprecatedRuleKeyDto;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.apache.commons.lang.math.RandomUtils.nextInt;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SingleDeprecatedRuleKeyTest {
+
+ @Test
+ public void test_creation_from_DeprecatedRuleKeyDto() {
+ // Creation from DeprecatedRuleKeyDto
+ DeprecatedRuleKeyDto deprecatedRuleKeyDto = new DeprecatedRuleKeyDto()
+ .setOldRuleKey(randomAlphanumeric(50))
+ .setOldRepositoryKey(randomAlphanumeric(50))
+ .setRuleId(nextInt(1000))
+ .setUuid(randomAlphanumeric(40));
+
+ SingleDeprecatedRuleKey singleDeprecatedRuleKey = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto);
+
+ assertThat(singleDeprecatedRuleKey.getOldRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getOldRepositoryKey());
+ assertThat(singleDeprecatedRuleKey.getOldRuleKey()).isEqualTo(deprecatedRuleKeyDto.getOldRuleKey());
+ assertThat(singleDeprecatedRuleKey.getNewRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getNewRepositoryKey());
+ assertThat(singleDeprecatedRuleKey.getNewRuleKey()).isEqualTo(deprecatedRuleKeyDto.getNewRuleKey());
+ assertThat(singleDeprecatedRuleKey.getUuid()).isEqualTo(deprecatedRuleKeyDto.getUuid());
+ assertThat(singleDeprecatedRuleKey.getRuleId()).isEqualTo(deprecatedRuleKeyDto.getRuleId());
+ assertThat(singleDeprecatedRuleKey.getOldRuleKeyAsRuleKey())
+ .isEqualTo(RuleKey.of(deprecatedRuleKeyDto.getOldRepositoryKey(), deprecatedRuleKeyDto.getOldRuleKey()));
+ }
+
+ @Test
+ public void test_creation_from_RulesDefinitionRule() {
+ // Creation from RulesDefinition.Rule
+ ImmutableSet<RuleKey> deprecatedRuleKeys = ImmutableSet.of(
+ RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)),
+ RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)),
+ RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50))
+ );
+
+ RulesDefinition.Repository repository = mock(RulesDefinition.Repository.class);
+ when(repository.key()).thenReturn(randomAlphanumeric(50));
+
+ RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class);
+ when(rule.key()).thenReturn(randomAlphanumeric(50));
+ when(rule.deprecatedRuleKeys()).thenReturn(deprecatedRuleKeys);
+ when(rule.repository()).thenReturn(repository);
+
+ Set<SingleDeprecatedRuleKey> singleDeprecatedRuleKeys = SingleDeprecatedRuleKey.from(rule);
+ assertThat(singleDeprecatedRuleKeys).hasSize(deprecatedRuleKeys.size());
+ assertThat(singleDeprecatedRuleKeys)
+ .extracting(SingleDeprecatedRuleKey::getUuid, SingleDeprecatedRuleKey::getOldRepositoryKey, SingleDeprecatedRuleKey::getOldRuleKey,
+ SingleDeprecatedRuleKey::getNewRepositoryKey, SingleDeprecatedRuleKey::getNewRuleKey, SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey)
+ .containsExactlyInAnyOrder(
+ deprecatedRuleKeys.stream().map(
+ r -> tuple(null, r.repository(), r.rule(), rule.repository().key(), rule.key(), RuleKey.of(r.repository(), r.rule()))
+ ).collect(MoreCollectors.toArrayList(deprecatedRuleKeys.size())).toArray(new Tuple[deprecatedRuleKeys.size()])
+ );
+ }
+
+ @Test
+ public void test_equality() {
+ DeprecatedRuleKeyDto deprecatedRuleKeyDto1 = new DeprecatedRuleKeyDto()
+ .setOldRuleKey(randomAlphanumeric(50))
+ .setOldRepositoryKey(randomAlphanumeric(50))
+ .setUuid(randomAlphanumeric(40))
+ .setRuleId(1);
+
+ DeprecatedRuleKeyDto deprecatedRuleKeyDto1WithoutUuid = new DeprecatedRuleKeyDto()
+ .setOldRuleKey(deprecatedRuleKeyDto1.getOldRuleKey())
+ .setOldRepositoryKey(deprecatedRuleKeyDto1.getOldRepositoryKey());
+
+ DeprecatedRuleKeyDto deprecatedRuleKeyDto2 = new DeprecatedRuleKeyDto()
+ .setOldRuleKey(randomAlphanumeric(50))
+ .setOldRepositoryKey(randomAlphanumeric(50))
+ .setUuid(randomAlphanumeric(40));
+
+ SingleDeprecatedRuleKey singleDeprecatedRuleKey1 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1);
+ SingleDeprecatedRuleKey singleDeprecatedRuleKey2 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2);
+
+ assertThat(singleDeprecatedRuleKey1).isEqualTo(singleDeprecatedRuleKey1);
+ assertThat(singleDeprecatedRuleKey1).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1));
+ assertThat(singleDeprecatedRuleKey1).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid));
+ assertThat(singleDeprecatedRuleKey2).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2));
+
+ assertThat(singleDeprecatedRuleKey1.hashCode()).isEqualTo(singleDeprecatedRuleKey1.hashCode());
+ assertThat(singleDeprecatedRuleKey1.hashCode()).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1).hashCode());
+ assertThat(singleDeprecatedRuleKey1.hashCode()).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid).hashCode());
+ assertThat(singleDeprecatedRuleKey2.hashCode()).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2).hashCode());
+
+ assertThat(singleDeprecatedRuleKey1).isNotEqualTo(null);
+ assertThat(singleDeprecatedRuleKey1).isNotEqualTo("");
+ assertThat(singleDeprecatedRuleKey1).isNotEqualTo(singleDeprecatedRuleKey2);
+ assertThat(singleDeprecatedRuleKey2).isNotEqualTo(singleDeprecatedRuleKey1);
+
+ assertThat(singleDeprecatedRuleKey1.hashCode()).isNotEqualTo(singleDeprecatedRuleKey2.hashCode());
+ assertThat(singleDeprecatedRuleKey2.hashCode()).isNotEqualTo(singleDeprecatedRuleKey1.hashCode());
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/WebServerRuleFinderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/WebServerRuleFinderImplTest.java
new file mode 100644
index 00000000000..5fdac77d9e8
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/WebServerRuleFinderImplTest.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.rule;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.db.DbClient;
+import org.sonar.db.rule.RuleDao;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class WebServerRuleFinderImplTest {
+
+ private DbClient dbClient = mock(DbClient.class);
+ private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.fromUuid("1111");
+ private WebServerRuleFinderImpl underTest = new WebServerRuleFinderImpl(dbClient, defaultOrganizationProvider);
+
+ @Before
+ public void setUp() throws Exception {
+ when(dbClient.ruleDao()).thenReturn(mock(RuleDao.class));
+ }
+
+ @Test
+ public void constructor_initializes_with_non_caching_delegate() {
+ assertThat(underTest.delegate).isInstanceOf(DefaultRuleFinder.class);
+ }
+
+ @Test
+ public void startCaching_sets_caching_delegate() {
+ underTest.startCaching();
+
+ assertThat(underTest.delegate).isInstanceOf(CachingRuleFinder.class);
+ }
+
+ @Test
+ public void stopCaching_restores_non_caching_delegate() {
+ RuleFinder nonCachingDelegate = underTest.delegate;
+
+ underTest.startCaching();
+ underTest.stopCaching();
+
+ assertThat(underTest.delegate).isSameAs(nonCachingDelegate);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/search/BaseDocTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/search/BaseDocTest.java
new file mode 100644
index 00000000000..450bf1b96d2
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/search/BaseDocTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.search;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.es.BaseDoc;
+import org.sonar.server.es.EsUtils;
+import org.sonar.server.es.Index;
+import org.sonar.server.es.IndexType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class BaseDocTest {
+ private final IndexType.IndexMainType someType = IndexType.main(Index.simple("bar"), "donut");
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void getField() {
+ Map<String, Object> fields = Maps.newHashMap();
+ fields.put("a_string", "foo");
+ fields.put("a_int", 42);
+ fields.put("a_null", null);
+ BaseDoc doc = new BaseDoc(someType, fields) {
+ @Override
+ public String getId() {
+ return null;
+ }
+
+ };
+
+ assertThat((String) doc.getNullableField("a_string")).isEqualTo("foo");
+ assertThat((int) doc.getNullableField("a_int")).isEqualTo(42);
+ assertThat((String) doc.getNullableField("a_null")).isNull();
+ }
+
+ @Test
+ public void getField_fails_if_missing_field() {
+ Map<String, Object> fields = Collections.emptyMap();
+ BaseDoc doc = new BaseDoc(someType, fields) {
+ @Override
+ public String getId() {
+ return null;
+ }
+
+ };
+
+ try {
+ doc.getNullableField("a_string");
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Field a_string not specified in query options");
+ }
+ }
+
+ @Test
+ public void getFieldAsDate() {
+ BaseDoc doc = new BaseDoc(someType, Maps.newHashMap()) {
+ @Override
+ public String getId() {
+ return null;
+ }
+
+ };
+ Date now = new Date();
+ doc.setField("javaDate", now);
+ assertThat(doc.getFieldAsDate("javaDate")).isEqualToIgnoringMillis(now);
+
+ doc.setField("stringDate", EsUtils.formatDateTime(now));
+ assertThat(doc.getFieldAsDate("stringDate")).isEqualToIgnoringMillis(now);
+ }
+
+ @Test
+ public void getNullableFieldAsDate() {
+ BaseDoc doc = new BaseDoc(someType, Maps.newHashMap()) {
+ @Override
+ public String getId() {
+ return null;
+ }
+
+ };
+ Date now = new Date();
+ doc.setField("javaDate", now);
+ assertThat(doc.getNullableFieldAsDate("javaDate")).isEqualToIgnoringMillis(now);
+
+ doc.setField("stringDate", EsUtils.formatDateTime(now));
+ assertThat(doc.getNullableFieldAsDate("stringDate")).isEqualToIgnoringMillis(now);
+
+ doc.setField("noValue", null);
+ assertThat(doc.getNullableFieldAsDate("noValue")).isNull();
+ }
+
+ @Test
+ public void getFields_fails_with_ISE_if_setParent_has_not_been_called_on_IndexRelationType() {
+ IndexType.IndexRelationType relationType = IndexType.relation(IndexType.main(Index.withRelations("foo"), "bar"), "donut");
+ BaseDoc doc = new BaseDoc(relationType) {
+
+ @Override
+ public String getId() {
+ throw new UnsupportedOperationException("getId not implemented");
+ }
+
+ };
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("parent must be set on a doc associated to a IndexRelationType (see BaseDoc#setParent(String))");
+
+ doc.getFields();
+ }
+
+ @Test
+ public void getFields_contains_join_field_and_indexType_field_when_setParent_has_been_called_on_IndexRelationType() {
+ Index index = Index.withRelations("foo");
+ IndexType.IndexRelationType relationType = IndexType.relation(IndexType.main(index, "bar"), "donut");
+ BaseDoc doc = new BaseDoc(relationType) {
+ {
+ setParent("miam");
+ }
+
+ @Override
+ public String getId() {
+ throw new UnsupportedOperationException("getId not implemented");
+ }
+
+ };
+
+ Map<String, Object> fields = doc.getFields();
+
+ assertThat((Map) fields.get(index.getJoinField()))
+ .isEqualTo(ImmutableMap.of("name", relationType.getName(), "parent", "miam"));
+ assertThat(fields.get("indexType")).isEqualTo(relationType.getName());
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java
new file mode 100644
index 00000000000..fb585a8565b
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.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.startup;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.server.plugins.InstalledPlugin;
+import org.sonar.server.plugins.InstalledPlugin.FileAndMd5;
+import org.sonar.server.plugins.PluginFileSystem;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class GeneratePluginIndexTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private ServerFileSystem serverFileSystem = mock(ServerFileSystem.class);
+ private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class);
+ private File index;
+
+ @Before
+ public void createIndexFile() throws IOException {
+ index = temp.newFile();
+ when(serverFileSystem.getPluginIndex()).thenReturn(index);
+ }
+
+ @Test
+ public void shouldWriteIndex() throws IOException {
+ InstalledPlugin javaPlugin = newInstalledPlugin("java", true);
+ InstalledPlugin gitPlugin = newInstalledPlugin("scmgit", false);
+ when(pluginFileSystem.getInstalledFiles()).thenReturn(asList(javaPlugin, gitPlugin));
+
+ GeneratePluginIndex underTest = new GeneratePluginIndex(serverFileSystem, pluginFileSystem);
+ underTest.start();
+
+ List<String> lines = FileUtils.readLines(index);
+ assertThat(lines).containsExactly(
+ "java,true," + javaPlugin.getLoadedJar().getFile().getName() + "|" + javaPlugin.getLoadedJar().getMd5(),
+ "scmgit,false," + gitPlugin.getLoadedJar().getFile().getName() + "|" + gitPlugin.getLoadedJar().getMd5());
+
+ underTest.stop();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void shouldThrowWhenUnableToWrite() throws IOException {
+ File wrongParent = temp.newFile();
+ wrongParent.createNewFile();
+ File wrongIndex = new File(wrongParent, "index.txt");
+ when(serverFileSystem.getPluginIndex()).thenReturn(wrongIndex);
+
+ new GeneratePluginIndex(serverFileSystem, pluginFileSystem).start();
+ }
+
+ private InstalledPlugin newInstalledPlugin(String key, boolean supportSonarLint) throws IOException {
+ FileAndMd5 jar = new FileAndMd5(temp.newFile());
+ PluginInfo pluginInfo = new PluginInfo(key).setJarFile(jar.getFile()).setSonarLintSupported(supportSonarLint);
+ return new InstalledPlugin(pluginInfo, jar, null);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/LogServerIdTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/LogServerIdTest.java
new file mode 100644
index 00000000000..073e9577710
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/LogServerIdTest.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.startup;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.platform.Server;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class LogServerIdTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Test
+ public void log_server_id_at_startup() {
+ Server server = mock(Server.class);
+ when(server.getId()).thenReturn("foo");
+
+ LogServerId underTest = new LogServerId(server);
+
+ underTest.start();
+ assertThat(logTester.logs(LoggerLevel.INFO)).contains("Server ID: foo");
+
+ // do not fail
+ underTest.stop();
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterMetricsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterMetricsTest.java
new file mode 100644
index 00000000000..b5fc51496cd
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterMetricsTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.startup;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.Metrics;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.metric.MetricDto;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class RegisterMetricsTest {
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ private DbClient dbClient = dbTester.getDbClient();
+
+ /**
+ * Insert new metrics, including custom metrics
+ */
+ @Test
+ public void insert_new_metrics() {
+ Metric m1 = new Metric.Builder("m1", "One", Metric.ValueType.FLOAT)
+ .setDescription("desc1")
+ .setDirection(1)
+ .setQualitative(true)
+ .setDomain("domain1")
+ .setUserManaged(false)
+ .create();
+ Metric custom = new Metric.Builder("custom", "Custom", Metric.ValueType.FLOAT)
+ .setDescription("This is a custom metric")
+ .setUserManaged(true)
+ .create();
+
+ RegisterMetrics register = new RegisterMetrics(dbClient);
+ register.register(asList(m1, custom));
+
+ Map<String, MetricDto> metricsByKey = selectAllMetrics();
+ assertThat(metricsByKey).hasSize(2);
+ assertEquals(m1, metricsByKey.get("m1"));
+ assertEquals(custom, metricsByKey.get("custom"));
+ }
+
+ /**
+ * Update existing metrics, except if custom metric
+ */
+ @Test
+ public void update_non_custom_metrics() {
+ dbTester.measures().insertMetric(t -> t.setKey("m1")
+ .setShortName("name")
+ .setValueType(Metric.ValueType.INT.name())
+ .setDescription("old desc")
+ .setDomain("old domain")
+ .setShortName("old short name")
+ .setQualitative(false)
+ .setUserManaged(false)
+ .setEnabled(true)
+ .setOptimizedBestValue(false)
+ .setDirection(1)
+ .setHidden(false));
+ MetricDto customMetric = dbTester.measures().insertMetric(t -> t.setKey("custom")
+ .setValueType(Metric.ValueType.FLOAT.name())
+ .setDescription("old desc")
+ .setShortName("Custom")
+ .setQualitative(false)
+ .setUserManaged(true)
+ .setEnabled(true)
+ .setOptimizedBestValue(false)
+ .setDirection(0)
+ .setHidden(false)
+ .setDecimalScale(1));
+
+ RegisterMetrics register = new RegisterMetrics(dbClient);
+ Metric m1 = new Metric.Builder("m1", "New name", Metric.ValueType.FLOAT)
+ .setDescription("new description")
+ .setDirection(-1)
+ .setQualitative(true)
+ .setDomain("new domain")
+ .setUserManaged(false)
+ .setDecimalScale(3)
+ .setHidden(true)
+ .create();
+ Metric custom = new Metric.Builder("custom", "New custom", Metric.ValueType.FLOAT)
+ .setDescription("New description of custom metric")
+ .setUserManaged(true)
+ .create();
+ register.register(asList(m1, custom));
+
+ Map<String, MetricDto> metricsByKey = selectAllMetrics();
+ assertThat(metricsByKey).hasSize(2);
+ assertEquals(m1, metricsByKey.get("m1"));
+ MetricDto actual = metricsByKey.get("custom");
+ assertThat(actual.getKey()).isEqualTo(custom.getKey());
+ assertThat(actual.getShortName()).isEqualTo(customMetric.getShortName());
+ assertThat(actual.getValueType()).isEqualTo(customMetric.getValueType());
+ assertThat(actual.getDescription()).isEqualTo(customMetric.getDescription());
+ assertThat(actual.getDirection()).isEqualTo(customMetric.getDirection());
+ assertThat(actual.isQualitative()).isEqualTo(customMetric.isQualitative());
+ assertThat(actual.isUserManaged()).isEqualTo(customMetric.isUserManaged());
+ }
+
+ @Test
+ public void disable_undefined_metrics() {
+ Random random = new Random();
+ int count = 1 + random.nextInt(10);
+ IntStream.range(0, count)
+ .forEach(t -> dbTester.measures().insertMetric(m -> m.setEnabled(random.nextBoolean()).setUserManaged(false)));
+
+ RegisterMetrics register = new RegisterMetrics(dbClient);
+ register.register(Collections.emptyList());
+
+ assertThat(selectAllMetrics().values().stream())
+ .extracting(MetricDto::isEnabled)
+ .containsOnly(IntStream.range(0, count).mapToObj(t -> false).toArray(Boolean[]::new));
+ }
+
+ @Test
+ public void enable_disabled_metrics() {
+ MetricDto enabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(true).setUserManaged(false));
+ MetricDto disabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(false).setUserManaged(false));
+
+ RegisterMetrics register = new RegisterMetrics(dbClient);
+ register.register(asList(builderOf(enabledMetric).create(), builderOf(disabledMetric).create()));
+
+ assertThat(selectAllMetrics().values())
+ .extracting(MetricDto::isEnabled)
+ .containsOnly(true, true);
+ }
+
+ @Test
+ public void does_not_enable_disabled_custom_metrics() {
+ MetricDto enabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(true).setUserManaged(true));
+ MetricDto disabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(false).setUserManaged(true));
+
+ RegisterMetrics register = new RegisterMetrics(dbClient);
+ register.register(asList(builderOf(enabledMetric).create(), builderOf(disabledMetric).create()));
+
+ assertThat(selectAllMetrics().values())
+ .extracting(MetricDto::getKey, MetricDto::isEnabled)
+ .containsOnly(
+ tuple(enabledMetric.getKey(), true),
+ tuple(disabledMetric.getKey(), false));
+ }
+
+ @Test
+ public void insert_core_metrics() {
+ RegisterMetrics register = new RegisterMetrics(dbClient);
+ register.start();
+
+ assertThat(dbTester.countRowsOfTable("metrics")).isEqualTo(CoreMetrics.getMetrics().size());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void fail_if_duplicated_plugin_metrics() {
+ Metrics plugin1 = new TestMetrics(new Metric.Builder("m1", "In first plugin", Metric.ValueType.FLOAT).create());
+ Metrics plugin2 = new TestMetrics(new Metric.Builder("m1", "In second plugin", Metric.ValueType.FLOAT).create());
+
+ new RegisterMetrics(dbClient, new Metrics[] {plugin1, plugin2}).start();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void fail_if_plugin_duplicates_core_metric() {
+ Metrics plugin = new TestMetrics(new Metric.Builder("ncloc", "In plugin", Metric.ValueType.FLOAT).create());
+
+ new RegisterMetrics(dbClient, new Metrics[] {plugin}).start();
+ }
+
+ private class TestMetrics implements Metrics {
+ private final List<Metric> metrics;
+
+ public TestMetrics(Metric... metrics) {
+ this.metrics = asList(metrics);
+ }
+
+ @Override
+ public List<Metric> getMetrics() {
+ return metrics;
+ }
+ }
+
+ private Map<String, MetricDto> selectAllMetrics() {
+ return dbTester.getDbClient().metricDao().selectAll(dbTester.getSession())
+ .stream()
+ .collect(uniqueIndex(MetricDto::getKey));
+ }
+
+ private void assertEquals(Metric expected, MetricDto actual) {
+ assertThat(actual.getKey()).isEqualTo(expected.getKey());
+ assertThat(actual.getShortName()).isEqualTo(expected.getName());
+ assertThat(actual.getValueType()).isEqualTo(expected.getType().name());
+ assertThat(actual.getDescription()).isEqualTo(expected.getDescription());
+ assertThat(actual.getDirection()).isEqualTo(expected.getDirection());
+ assertThat(actual.isQualitative()).isEqualTo(expected.getQualitative());
+ assertThat(actual.isUserManaged()).isEqualTo(expected.getUserManaged());
+ }
+
+ private static Metric.Builder builderOf(MetricDto enabledMetric) {
+ return new Metric.Builder(enabledMetric.getKey(), enabledMetric.getShortName(), Metric.ValueType.valueOf(enabledMetric.getValueType()))
+ .setDescription(enabledMetric.getDescription())
+ .setDirection(enabledMetric.getDirection())
+ .setQualitative(enabledMetric.isQualitative())
+ .setQualitative(enabledMetric.isQualitative())
+ .setDomain(enabledMetric.getDomain())
+ .setUserManaged(enabledMetric.isUserManaged())
+ .setHidden(enabledMetric.isHidden());
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPermissionTemplatesTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPermissionTemplatesTest.java
new file mode 100644
index 00000000000..b7683ee184d
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPermissionTemplatesTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.startup;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.security.DefaultGroups;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbTester;
+import org.sonar.db.organization.DefaultTemplates;
+import org.sonar.db.permission.OrganizationPermission;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.permission.template.PermissionTemplateGroupDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.permission.template.PermissionTemplateTesting.newPermissionTemplateDto;
+
+public class RegisterPermissionTemplatesTest {
+ private static final String DEFAULT_TEMPLATE_UUID = "default_template";
+
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+ @Rule
+ public LogTester logTester = new LogTester();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
+ private ResourceTypes resourceTypes = mock(ResourceTypes.class);
+ private RegisterPermissionTemplates underTest = new RegisterPermissionTemplates(db.getDbClient(), defaultOrganizationProvider);
+
+ @Test
+ public void fail_with_ISE_if_default_template_must_be_created_and_no_default_group_is_defined() {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Default group for organization " + db.getDefaultOrganization().getUuid() + " is not defined");
+
+ underTest.start();
+ }
+
+ @Test
+ public void fail_with_ISE_if_default_template_must_be_created_and_default_group_does_not_exist() {
+ setDefaultGroupId(new GroupDto().setId(22));
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Default group with id 22 for organization " + db.getDefaultOrganization().getUuid() + " doesn't exist");
+
+ underTest.start();
+ }
+
+ @Test
+ public void insert_default_permission_template_if_fresh_install_without_governance() {
+ GroupDto defaultGroup = createAndSetDefaultGroup();
+ db.users().insertGroup(db.getDefaultOrganization(), DefaultGroups.ADMINISTRATORS);
+
+ when(resourceTypes.isQualifierPresent(eq(Qualifiers.APP))).thenReturn(false);
+ when(resourceTypes.isQualifierPresent(eq(Qualifiers.VIEW))).thenReturn(false);
+ underTest.start();
+
+ PermissionTemplateDto defaultTemplate = selectTemplate();
+ assertThat(defaultTemplate.getName()).isEqualTo("Default template");
+
+ List<PermissionTemplateGroupDto> groupPermissions = selectGroupPermissions(defaultTemplate);
+ assertThat(groupPermissions).hasSize(7);
+ expectGroupPermission(groupPermissions, UserRole.ADMIN, DefaultGroups.ADMINISTRATORS);
+ expectGroupPermission(groupPermissions, OrganizationPermission.APPLICATION_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS);
+ expectGroupPermission(groupPermissions, OrganizationPermission.PORTFOLIO_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS);
+ expectGroupPermission(groupPermissions, UserRole.CODEVIEWER, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.USER, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.ISSUE_ADMIN, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup.getName());
+
+ verifyDefaultTemplates();
+
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ }
+
+ @Test
+ public void insert_default_permission_template_if_fresh_install_with_governance() {
+ GroupDto defaultGroup = createAndSetDefaultGroup();
+ db.users().insertGroup(db.getDefaultOrganization(), DefaultGroups.ADMINISTRATORS);
+
+ when(resourceTypes.isQualifierPresent(eq(Qualifiers.APP))).thenReturn(true);
+ when(resourceTypes.isQualifierPresent(eq(Qualifiers.VIEW))).thenReturn(true);
+ underTest.start();
+
+ PermissionTemplateDto defaultTemplate = selectTemplate();
+ assertThat(defaultTemplate.getName()).isEqualTo("Default template");
+
+ List<PermissionTemplateGroupDto> groupPermissions = selectGroupPermissions(defaultTemplate);
+ assertThat(groupPermissions).hasSize(7);
+ expectGroupPermission(groupPermissions, UserRole.ADMIN, DefaultGroups.ADMINISTRATORS);
+ expectGroupPermission(groupPermissions, OrganizationPermission.APPLICATION_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS);
+ expectGroupPermission(groupPermissions, OrganizationPermission.PORTFOLIO_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS);
+ expectGroupPermission(groupPermissions, UserRole.CODEVIEWER, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.USER, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.ISSUE_ADMIN, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup.getName());
+
+ verifyDefaultTemplates();
+
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ }
+
+ @Test
+ public void ignore_administrators_permissions_if_group_does_not_exist() {
+ GroupDto defaultGroup = createAndSetDefaultGroup();
+
+ underTest.start();
+
+ PermissionTemplateDto defaultTemplate = selectTemplate();
+ assertThat(defaultTemplate.getName()).isEqualTo("Default template");
+
+ List<PermissionTemplateGroupDto> groupPermissions = selectGroupPermissions(defaultTemplate);
+ assertThat(groupPermissions).hasSize(4);
+ expectGroupPermission(groupPermissions, UserRole.CODEVIEWER, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.USER, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.ISSUE_ADMIN, defaultGroup.getName());
+ expectGroupPermission(groupPermissions, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup.getName());
+
+ verifyDefaultTemplates();
+
+ assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Cannot setup default permission for group: sonar-administrators");
+ }
+
+ @Test
+ public void do_not_create_default_template_if_already_exists_but_register_when_it_is_not() {
+ db.permissionTemplates().insertTemplate(newPermissionTemplateDto()
+ .setOrganizationUuid(db.getDefaultOrganization().getUuid())
+ .setUuid(DEFAULT_TEMPLATE_UUID));
+
+ underTest.start();
+
+ verifyDefaultTemplates();
+ }
+
+ @Test
+ public void do_not_fail_if_default_template_exists_and_is_registered() {
+ PermissionTemplateDto projectTemplate = db.permissionTemplates().insertTemplate(newPermissionTemplateDto()
+ .setOrganizationUuid(db.getDefaultOrganization().getUuid())
+ .setUuid(DEFAULT_TEMPLATE_UUID));
+ db.organizations().setDefaultTemplates(projectTemplate, null, null);
+
+ underTest.start();
+
+ verifyDefaultTemplates();
+ }
+
+ private PermissionTemplateDto selectTemplate() {
+ return db.getDbClient().permissionTemplateDao().selectByUuid(db.getSession(), DEFAULT_TEMPLATE_UUID);
+ }
+
+ private List<PermissionTemplateGroupDto> selectGroupPermissions(PermissionTemplateDto template) {
+ return db.getDbClient().permissionTemplateDao().selectGroupPermissionsByTemplateId(db.getSession(), template.getId());
+ }
+
+ private void expectGroupPermission(List<PermissionTemplateGroupDto> groupPermissions, String expectedPermission,
+ String expectedGroupName) {
+ assertThat(
+ groupPermissions.stream().anyMatch(gp -> gp.getPermission().equals(expectedPermission) && Objects.equals(gp.getGroupName(), expectedGroupName)))
+ .isTrue();
+ }
+
+ private void verifyDefaultTemplates() {
+ Optional<DefaultTemplates> defaultTemplates = db.getDbClient().organizationDao().getDefaultTemplates(db.getSession(), db.getDefaultOrganization().getUuid());
+ assertThat(defaultTemplates)
+ .isPresent();
+ assertThat(defaultTemplates.get().getProjectUuid()).isEqualTo(DEFAULT_TEMPLATE_UUID);
+ }
+
+ private void setDefaultGroupId(GroupDto defaultGroup) {
+ db.getDbClient().organizationDao().setDefaultGroupId(db.getSession(), db.getDefaultOrganization().getUuid(), defaultGroup);
+ db.commit();
+ }
+
+ private GroupDto createAndSetDefaultGroup() {
+ GroupDto res = db.users().insertGroup(db.getDefaultOrganization());
+ setDefaultGroupId(res);
+ return res;
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java
new file mode 100644
index 00000000000..9fed6eef493
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.startup;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.utils.System2;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.plugin.PluginDto;
+import org.sonar.server.plugins.InstalledPlugin;
+import org.sonar.server.plugins.PluginFileSystem;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class RegisterPluginsTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ private final long now = 12345L;
+ private DbClient dbClient = dbTester.getDbClient();
+ private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class);
+ private UuidFactory uuidFactory = mock(UuidFactory.class);
+ private System2 system2 = mock(System2.class);
+
+ @Before
+ public void setUp() {
+ when(system2.now()).thenReturn(now).thenThrow(new IllegalStateException("Should be called only once"));
+ }
+
+ /**
+ * Insert new plugins
+ */
+ @Test
+ public void insert_new_plugins() throws IOException {
+ File fakeJavaJar = temp.newFile();
+ FileUtils.write(fakeJavaJar, "fakejava", StandardCharsets.UTF_8);
+ File fakeJavaCustomJar = temp.newFile();
+ FileUtils.write(fakeJavaCustomJar, "fakejavacustom", StandardCharsets.UTF_8);
+ when(pluginFileSystem.getInstalledFiles()).thenReturn(asList(
+ newPlugin("java", fakeJavaJar, null),
+ newPlugin("javacustom", fakeJavaCustomJar, "java")));
+ when(uuidFactory.create()).thenReturn("a").thenReturn("b").thenThrow(new IllegalStateException("Should be called only twice"));
+ RegisterPlugins register = new RegisterPlugins(pluginFileSystem, dbClient, uuidFactory, system2);
+ register.start();
+
+ Map<String, PluginDto> pluginsByKey = selectAllPlugins();
+ assertThat(pluginsByKey).hasSize(2);
+ verify(pluginsByKey.get("java"), null, "bd451e47a1aa76e73da0359cef63dd63", now, now);
+ verify(pluginsByKey.get("javacustom"), "java", "de9b2de3ddc0680904939686c0dba5be", now, now);
+
+ register.stop();
+ }
+
+ /**
+ * Update existing plugins, only when checksum is different and don't remove uninstalled plugins
+ */
+ @Test
+ public void update_only_changed_plugins() throws IOException {
+ dbClient.pluginDao().insert(dbTester.getSession(), new PluginDto()
+ .setUuid("a")
+ .setKee("java")
+ .setBasePluginKey(null)
+ .setFileHash("bd451e47a1aa76e73da0359cef63dd63")
+ .setCreatedAt(1L)
+ .setUpdatedAt(1L));
+ dbClient.pluginDao().insert(dbTester.getSession(), new PluginDto()
+ .setUuid("b")
+ .setKee("javacustom")
+ .setBasePluginKey("java")
+ .setFileHash("de9b2de3ddc0680904939686c0dba5be")
+ .setCreatedAt(1L)
+ .setUpdatedAt(1L));
+ dbTester.commit();
+
+ File fakeJavaCustomJar = temp.newFile();
+ FileUtils.write(fakeJavaCustomJar, "fakejavacustomchanged", StandardCharsets.UTF_8);
+ when(pluginFileSystem.getInstalledFiles()).thenReturn(asList(
+ newPlugin("javacustom", fakeJavaCustomJar, "java2")));
+
+ new RegisterPlugins(pluginFileSystem, dbClient, uuidFactory, system2).start();
+
+ Map<String, PluginDto> pluginsByKey = selectAllPlugins();
+ assertThat(pluginsByKey).hasSize(2);
+ verify(pluginsByKey.get("java"), null, "bd451e47a1aa76e73da0359cef63dd63", 1L, 1L);
+ verify(pluginsByKey.get("javacustom"), "java2", "d22091cff5155e892cfe2f9dab51f811", 1L, now);
+ }
+
+ private static InstalledPlugin newPlugin(String key, File file, @Nullable String basePlugin) {
+ InstalledPlugin.FileAndMd5 jar = new InstalledPlugin.FileAndMd5(file);
+ PluginInfo info = new PluginInfo(key)
+ .setBasePlugin(basePlugin)
+ .setJarFile(file);
+ return new InstalledPlugin(info, jar, null);
+ }
+
+ private Map<String, PluginDto> selectAllPlugins() {
+ return dbTester.getDbClient().pluginDao().selectAll(dbTester.getSession())
+ .stream()
+ .collect(uniqueIndex(PluginDto::getKee));
+ }
+
+ private void verify(PluginDto java, @Nullable String basePluginKey, String fileHash, @Nullable Long createdAt, long updatedAt) {
+ assertThat(java.getBasePluginKey()).isEqualTo(basePluginKey);
+ assertThat(java.getFileHash()).isEqualTo(fileHash);
+ assertThat(java.getCreatedAt()).isEqualTo(createdAt);
+ assertThat(java.getUpdatedAt()).isEqualTo(updatedAt);
+ }
+
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java
new file mode 100644
index 00000000000..0edd9f2d27e
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.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.startup;
+
+import org.junit.Test;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.db.property.PropertiesDao;
+
+import static org.mockito.Mockito.*;
+
+public class RenameDeprecatedPropertyKeysTest {
+ @Test
+ public void should_rename_deprecated_keys() {
+ PropertiesDao dao = mock(PropertiesDao.class);
+ PropertyDefinitions definitions = new PropertyDefinitions(FakeExtension.class);
+ RenameDeprecatedPropertyKeys task = new RenameDeprecatedPropertyKeys(dao, definitions);
+ task.start();
+
+ verify(dao).renamePropertyKey("old_key", "new_key");
+ verifyNoMoreInteractions(dao);
+ }
+
+ @Properties({
+ @Property(key = "new_key", deprecatedKey = "old_key", name = "Name"),
+ @Property(key = "other", name = "Other")
+ })
+ public static class FakeExtension {
+
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/FakeServer.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/FakeServer.java
new file mode 100644
index 00000000000..b1aa8a872ce
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/FakeServer.java
@@ -0,0 +1,98 @@
+/*
+ * 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.io.File;
+import java.util.Date;
+import javax.annotation.CheckForNull;
+import org.sonar.api.platform.Server;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+
+class FakeServer extends Server {
+ private String id;
+ private String version;
+
+ public FakeServer() {
+ this.id = randomAlphanumeric(20);
+ this.version = randomAlphanumeric(10);
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ FakeServer setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ @CheckForNull
+ @Override
+ public String getPermanentServerId() {
+ return null;
+ }
+
+ @Override
+ public String getVersion() {
+ return this.version;
+ }
+
+ public FakeServer setVersion(String version) {
+ this.version = version;
+ return this;
+ }
+
+ @Override
+ public Date getStartedAt() {
+ return null;
+ }
+
+ @Override
+ public File getRootDir() {
+ return null;
+ }
+
+ @Override
+ public String getContextPath() {
+ return null;
+ }
+
+ @Override
+ public String getPublicRootUrl() {
+ return null;
+ }
+
+ @Override
+ public boolean isDev() {
+ return false;
+ }
+
+ @Override
+ public boolean isSecured() {
+ return false;
+ }
+
+ @Override
+ public String getURL() {
+ return null;
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryClientTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryClientTest.java
new file mode 100644
index 00000000000..60fdc81e5d6
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryClientTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.io.IOException;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okio.Buffer;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.config.internal.MapSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL;
+
+public class TelemetryClientTest {
+
+ private static final String JSON = "{\"key\":\"value\"}";
+ private static final String TELEMETRY_URL = "https://telemetry.com/url";
+
+ private OkHttpClient okHttpClient = mock(OkHttpClient.class, RETURNS_DEEP_STUBS);
+ private MapSettings settings = new MapSettings();
+
+ private TelemetryClient underTest = new TelemetryClient(okHttpClient, settings.asConfig());
+
+ @Test
+ public void upload() throws IOException {
+ ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
+ settings.setProperty(SONAR_TELEMETRY_URL.getKey(), TELEMETRY_URL);
+ underTest.start();
+
+ underTest.upload(JSON);
+
+ verify(okHttpClient).newCall(requestCaptor.capture());
+ Request request = requestCaptor.getValue();
+ assertThat(request.method()).isEqualTo("POST");
+ assertThat(request.body().contentType()).isEqualTo(MediaType.parse("application/json; charset=utf-8"));
+ Buffer body = new Buffer();
+ request.body().writeTo(body);
+ assertThat(body.readUtf8()).isEqualTo(JSON);
+ assertThat(request.url().toString()).isEqualTo(TELEMETRY_URL);
+ }
+
+ @Test
+ public void opt_out() throws IOException {
+ ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
+ settings.setProperty(SONAR_TELEMETRY_URL.getKey(), TELEMETRY_URL);
+ underTest.start();
+
+ underTest.optOut(JSON);
+
+ verify(okHttpClient).newCall(requestCaptor.capture());
+ Request request = requestCaptor.getValue();
+ assertThat(request.method()).isEqualTo("DELETE");
+ assertThat(request.body().contentType()).isEqualTo(MediaType.parse("application/json; charset=utf-8"));
+ Buffer body = new Buffer();
+ request.body().writeTo(body);
+ assertThat(body.readUtf8()).isEqualTo(JSON);
+ assertThat(request.url().toString()).isEqualTo(TELEMETRY_URL);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java
new file mode 100644
index 00000000000..dc57204baf2
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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.io.IOException;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.core.platform.EditionProvider;
+import org.sonar.core.platform.PlatformEditionProvider;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginRepository;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.measure.index.ProjectMeasuresIndex;
+import org.sonar.server.measure.index.ProjectMeasuresIndexer;
+import org.sonar.server.organization.DefaultOrganizationProviderImpl;
+import org.sonar.server.property.InternalProperties;
+import org.sonar.server.property.MapInternalProperties;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.util.GlobalLockManager;
+import org.sonar.updatecenter.common.Version;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
+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.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
+import static org.sonar.api.measures.CoreMetrics.LINES_KEY;
+import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY;
+import static org.sonar.api.measures.CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION_KEY;
+import static org.sonar.api.utils.DateUtils.parseDate;
+import static org.sonar.db.component.BranchType.LONG;
+import static org.sonar.db.component.BranchType.SHORT;
+import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_ENABLE;
+import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_FREQUENCY_IN_SECONDS;
+import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL;
+import static org.sonar.test.JsonAssert.assertJson;
+
+public class TelemetryDaemonTest {
+
+ private static final long ONE_HOUR = 60 * 60 * 1_000L;
+ private static final long ONE_DAY = 24 * ONE_HOUR;
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ @Rule
+ public DbTester db = DbTester.create();
+ @Rule
+ public EsTester es = EsTester.create();
+ @Rule
+ public LogTester logger = new LogTester().setLevel(LoggerLevel.DEBUG);
+
+ private TelemetryClient client = mock(TelemetryClient.class);
+ private InternalProperties internalProperties = spy(new MapInternalProperties());
+ private final GlobalLockManager lockManager = mock(GlobalLockManager.class);
+ private FakeServer server = new FakeServer();
+ private PluginRepository pluginRepository = mock(PluginRepository.class);
+ private TestSystem2 system2 = new TestSystem2().setNow(System.currentTimeMillis());
+ private MapSettings settings = new MapSettings();
+ private ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(db.getDbClient(), es.client());
+ private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
+ private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
+
+ private final TelemetryDataLoader communityDataLoader = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, new UserIndex(es.client(), system2),
+ new ProjectMeasuresIndex(es.client(), null, system2), editionProvider, new DefaultOrganizationProviderImpl(db.getDbClient()), internalProperties, null);
+ private TelemetryDaemon communityUnderTest = new TelemetryDaemon(communityDataLoader, client, settings.asConfig(), internalProperties, lockManager, system2);
+
+ private final LicenseReader licenseReader = mock(LicenseReader.class);
+ private final TelemetryDataLoader commercialDataLoader = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, new UserIndex(es.client(), system2),
+ new ProjectMeasuresIndex(es.client(), null, system2), editionProvider, new DefaultOrganizationProviderImpl(db.getDbClient()), internalProperties, licenseReader);
+ private TelemetryDaemon commercialUnderTest = new TelemetryDaemon(commercialDataLoader, client, settings.asConfig(), internalProperties, lockManager, system2);
+
+ @After
+ public void tearDown() {
+ communityUnderTest.stop();
+ }
+
+ @Test
+ public void send_telemetry_data() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ server.setId("AU-TpxcB-iU5OvuD2FL7");
+ server.setVersion("7.5.4");
+ List<PluginInfo> plugins = asList(newPlugin("java", "4.12.0.11033"), newPlugin("scmgit", "1.2"), new PluginInfo("other"));
+ when(pluginRepository.getPluginInfos()).thenReturn(plugins);
+ when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+
+ IntStream.range(0, 3).forEach(i -> db.users().insertUser());
+ db.users().insertUser(u -> u.setActive(false));
+ userIndexer.indexOnStartup(emptySet());
+
+ MetricDto lines = db.measures().insertMetric(m -> m.setKey(LINES_KEY));
+ MetricDto ncloc = db.measures().insertMetric(m -> m.setKey(NCLOC_KEY));
+ MetricDto coverage = db.measures().insertMetric(m -> m.setKey(COVERAGE_KEY));
+ MetricDto nclocDistrib = db.measures().insertMetric(m -> m.setKey(NCLOC_LANGUAGE_DISTRIBUTION_KEY));
+
+ ComponentDto project1 = db.components().insertMainBranch(db.getDefaultOrganization());
+ ComponentDto project1Branch = db.components().insertProjectBranch(project1);
+ db.measures().insertLiveMeasure(project1, lines, m -> m.setValue(200d));
+ db.measures().insertLiveMeasure(project1, ncloc, m -> m.setValue(100d));
+ db.measures().insertLiveMeasure(project1, coverage, m -> m.setValue(80d));
+ db.measures().insertLiveMeasure(project1, nclocDistrib, m -> m.setValue(null).setData("java=200;js=50"));
+
+ ComponentDto project2 = db.components().insertMainBranch(db.getDefaultOrganization());
+ db.measures().insertLiveMeasure(project2, lines, m -> m.setValue(300d));
+ db.measures().insertLiveMeasure(project2, ncloc, m -> m.setValue(200d));
+ db.measures().insertLiveMeasure(project2, coverage, m -> m.setValue(80d));
+ db.measures().insertLiveMeasure(project2, nclocDistrib, m -> m.setValue(null).setData("java=300;kotlin=2500"));
+ projectMeasuresIndexer.indexOnStartup(emptySet());
+
+ communityUnderTest.start();
+
+ ArgumentCaptor<String> jsonCaptor = captureJson();
+ String json = jsonCaptor.getValue();
+ assertJson(json).ignoreFields("database").isSimilarTo(getClass().getResource("telemetry-example.json"));
+ assertJson(getClass().getResource("telemetry-example.json")).ignoreFields("database").isSimilarTo(json);
+ assertDatabaseMetadata(json);
+ assertThat(logger.logs(LoggerLevel.INFO)).contains("Sharing of SonarQube statistics is enabled.");
+ }
+
+ private void assertDatabaseMetadata(String json) {
+ try (DbSession dbSession = db.getDbClient().openSession(false)) {
+ DatabaseMetaData metadata = dbSession.getConnection().getMetaData();
+ assertJson(json).isSimilarTo("{\n" +
+ " \"database\": {\n" +
+ " \"name\": \"H2\",\n" +
+ " \"version\": \"" + metadata.getDatabaseProductVersion() + "\"\n" +
+ " }\n" +
+ "}");
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void take_biggest_long_living_branches() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ server.setId("AU-TpxcB-iU5OvuD2FL7").setVersion("7.5.4");
+ MetricDto ncloc = db.measures().insertMetric(m -> m.setKey(NCLOC_KEY));
+ ComponentDto project = db.components().insertMainBranch(db.getDefaultOrganization());
+ ComponentDto longBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(LONG));
+ ComponentDto shortBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(SHORT));
+ db.measures().insertLiveMeasure(project, ncloc, m -> m.setValue(10d));
+ db.measures().insertLiveMeasure(longBranch, ncloc, m -> m.setValue(20d));
+ db.measures().insertLiveMeasure(shortBranch, ncloc, m -> m.setValue(30d));
+ projectMeasuresIndexer.indexOnStartup(emptySet());
+
+ communityUnderTest.start();
+
+ ArgumentCaptor<String> jsonCaptor = captureJson();
+ assertJson(jsonCaptor.getValue()).isSimilarTo("{\n" +
+ " \"ncloc\": 20\n" +
+ "}\n");
+ }
+
+ @Test
+ public void send_data_via_client_at_startup_after_initial_delay() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ communityUnderTest.start();
+
+ verify(client, timeout(2_000).atLeastOnce()).upload(anyString());
+ }
+
+ @Test
+ public void data_contains_no_license_type_on_community_edition() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+
+ communityUnderTest.start();
+
+ ArgumentCaptor<String> jsonCaptor = captureJson();
+ assertThat(jsonCaptor.getValue()).doesNotContain("licenseType");
+ }
+
+ @Test
+ public void data_contains_no_license_type_on_commercial_edition_if_no_license() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ when(licenseReader.read()).thenReturn(Optional.empty());
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+
+ commercialUnderTest.start();
+
+ ArgumentCaptor<String> jsonCaptor = captureJson();
+ assertThat(jsonCaptor.getValue()).doesNotContain("licenseType");
+ }
+
+ @Test
+ public void data_has_license_type_on_commercial_edition_if_no_license() throws IOException {
+ String licenseType = randomAlphabetic(12);
+ initTelemetrySettingsToDefaultValues();
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ LicenseReader.License license = mock(LicenseReader.License.class);
+ when(license.getType()).thenReturn(licenseType);
+ when(licenseReader.read()).thenReturn(Optional.of(license));
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+
+ commercialUnderTest.start();
+
+ ArgumentCaptor<String> jsonCaptor = captureJson();
+ assertJson(jsonCaptor.getValue()).isSimilarTo("{\n" +
+ " \"licenseType\": \"" + licenseType + "\"\n" +
+ "}\n");
+ }
+
+ @Test
+ public void check_if_should_send_data_periodically() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ long now = system2.now();
+ long sixDaysAgo = now - (ONE_DAY * 6L);
+ long sevenDaysAgo = now - (ONE_DAY * 7L);
+ internalProperties.write("telemetry.lastPing", String.valueOf(sixDaysAgo));
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ communityUnderTest.start();
+ verify(client, after(2_000).never()).upload(anyString());
+ internalProperties.write("telemetry.lastPing", String.valueOf(sevenDaysAgo));
+
+ verify(client, timeout(2_000).atLeastOnce()).upload(anyString());
+ }
+
+ @Test
+ public void send_server_id_and_version() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ String id = randomAlphanumeric(40);
+ String version = randomAlphanumeric(10);
+ server.setId(id);
+ server.setVersion(version);
+ communityUnderTest.start();
+
+ ArgumentCaptor<String> json = captureJson();
+ assertThat(json.getValue()).contains(id, version);
+ }
+
+ @Test
+ public void send_server_installation_date_and_installation_version() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ String installationVersion = "7.9.BEST.LTS.EVER";
+ Long installationDate = 1546300800000L; // 2019/01/01
+ internalProperties.write(InternalProperties.INSTALLATION_DATE, String.valueOf(installationDate));
+ internalProperties.write(InternalProperties.INSTALLATION_VERSION, installationVersion);
+
+ communityUnderTest.start();
+
+ ArgumentCaptor<String> json = captureJson();
+ assertThat(json.getValue()).contains(installationVersion, installationDate.toString());
+ }
+
+ @Test
+ public void do_not_send_server_installation_details_if_missing_property() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+
+ communityUnderTest.start();
+
+ ArgumentCaptor<String> json = captureJson();
+ assertThat(json.getValue()).doesNotContain("installationVersion", "installationDate");
+ }
+
+ @Test
+ public void do_not_send_data_if_last_ping_earlier_than_one_week_ago() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ long now = system2.now();
+ long sixDaysAgo = now - (ONE_DAY * 6L);
+
+ internalProperties.write("telemetry.lastPing", String.valueOf(sixDaysAgo));
+ communityUnderTest.start();
+
+ verify(client, after(2_000).never()).upload(anyString());
+ }
+
+ @Test
+ public void send_data_if_last_ping_is_one_week_ago() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ long today = parseDate("2017-08-01").getTime();
+ system2.setNow(today + 15 * ONE_HOUR);
+ long sevenDaysAgo = today - (ONE_DAY * 7L);
+ internalProperties.write("telemetry.lastPing", String.valueOf(sevenDaysAgo));
+ reset(internalProperties);
+
+ communityUnderTest.start();
+
+ verify(internalProperties, timeout(4_000)).write("telemetry.lastPing", String.valueOf(today));
+ verify(client).upload(anyString());
+ }
+
+ @Test
+ public void opt_out_sent_once() throws IOException {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ settings.setProperty("sonar.telemetry.enable", "false");
+ communityUnderTest.start();
+ communityUnderTest.start();
+
+ verify(client, after(2_000).never()).upload(anyString());
+ verify(client, timeout(2_000).times(1)).optOut(anyString());
+ assertThat(logger.logs(LoggerLevel.INFO)).contains("Sharing of SonarQube statistics is disabled.");
+ }
+
+ private PluginInfo newPlugin(String key, String version) {
+ return new PluginInfo(key)
+ .setVersion(Version.create(version));
+ }
+
+ private void initTelemetrySettingsToDefaultValues() {
+ settings.setProperty(SONAR_TELEMETRY_ENABLE.getKey(), SONAR_TELEMETRY_ENABLE.getDefaultValue());
+ settings.setProperty(SONAR_TELEMETRY_URL.getKey(), SONAR_TELEMETRY_URL.getDefaultValue());
+ settings.setProperty(SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getKey(), SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getDefaultValue());
+ }
+
+ private ArgumentCaptor<String> captureJson() throws IOException {
+ ArgumentCaptor<String> jsonCaptor = ArgumentCaptor.forClass(String.class);
+ verify(client, timeout(2_000).atLeastOnce()).upload(jsonCaptor.capture());
+ return jsonCaptor;
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/updatecenter/UpdateCenterModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/updatecenter/UpdateCenterModuleTest.java
new file mode 100644
index 00000000000..b7531b0f39b
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/updatecenter/UpdateCenterModuleTest.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.updatecenter;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class UpdateCenterModuleTest {
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new UpdateCenterModule().configure(container);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2);
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/util/DateCollectorTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/util/DateCollectorTest.java
new file mode 100644
index 00000000000..e3d0146c112
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/util/DateCollectorTest.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 org.junit.Test;
+import org.sonar.api.utils.DateUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DateCollectorTest {
+
+ DateCollector collector = new DateCollector();
+
+ @Test
+ public void max_is_zero_if_no_dates() {
+ assertThat(collector.getMax()).isEqualTo(0L);
+ }
+
+ @Test
+ public void max() {
+ collector.add(DateUtils.parseDate("2013-06-01"));
+ collector.add(null);
+ collector.add(DateUtils.parseDate("2014-01-01"));
+ collector.add(DateUtils.parseDate("2013-08-01"));
+
+ assertThat(collector.getMax()).isEqualTo(DateUtils.parseDateQuietly("2014-01-01").getTime());
+ }
+}
diff --git a/server/sonar-webserver-core/src/test/resources/logback-test.xml b/server/sonar-webserver-core/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..3e34b0f9fc8
--- /dev/null
+++ b/server/sonar-webserver-core/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>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/csharp-model.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/csharp-model.xml
new file mode 100644
index 00000000000..e4569a2a7bf
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/csharp-model.xml
@@ -0,0 +1,25 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <rule-repo>gendarme</rule-repo>
+ <rule-key>EnsureLocalDisposalRule</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>0.125</val>
+ <txt>d</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+
+</sqale> \ No newline at end of file
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/java-model.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/java-model.xml
new file mode 100644
index 00000000000..0b37f562107
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/java-model.xml
@@ -0,0 +1,25 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <rule-repo>squid-cobol</rule-repo>
+ <rule-key>CheckLoop</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>0.125</val>
+ <txt>d</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+
+</sqale> \ No newline at end of file
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelXMLExporterTest/export_xml.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelXMLExporterTest/export_xml.xml
new file mode 100644
index 00000000000..ef18e12ef83
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelXMLExporterTest/export_xml.xml
@@ -0,0 +1,20 @@
+<sqale>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>LINEAR_OFFSET</txt>
+ </prop>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3</val>
+ <txt>d</txt>
+ </prop>
+ <prop>
+ <key>offset</key>
+ <val>15</val>
+ <txt>min</txt>
+ </prop>
+ </chc>
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml
new file mode 100644
index 00000000000..00deb4d0299
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml
@@ -0,0 +1,50 @@
+<!--
+ ~ SonarQube, open source software quality management tool.
+ ~ Copyright (C) 2008-2016 SonarSource
+ ~ mailto:contact AT sonarsource DOT com
+ ~
+ ~ SonarQube 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.
+ ~
+ ~ SonarQube 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.
+ -->
+
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>constant_issue</txt>
+ </prop>
+ <prop>
+ <!-- Should be replaced by offset -->
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_deprecated_linear_with_threshold_function_by_linear_function.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_deprecated_linear_with_threshold_function_by_linear_function.xml
new file mode 100644
index 00000000000..9ebc69b94a6
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_deprecated_linear_with_threshold_function_by_linear_function.xml
@@ -0,0 +1,36 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFunction</key>
+ <!-- Should be replaced by linear -->
+ <txt>linear_threshold</txt>
+ </prop>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <!-- Should be ignored -->
+ <prop>
+ <key>offset</key>
+ <val>1.0</val>
+ <txt>h</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_network_use_key.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_network_use_key.xml
new file mode 100644
index 00000000000..d4707ba62a8
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_network_use_key.xml
@@ -0,0 +1,23 @@
+<sqale>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>NETWORK_USE_EFFICIENCY</key>
+ <name>Network use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>LINEAR</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/fail_on_bad_xml.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/fail_on_bad_xml.xml
new file mode 100644
index 00000000000..3b15eae11d6
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/fail_on_bad_xml.xml
@@ -0,0 +1 @@
+Not a valid xml
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_deprecated_constant_per_file_function.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_deprecated_constant_per_file_function.xml
new file mode 100644
index 00000000000..4b8ae3f6475
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_deprecated_constant_per_file_function.xml
@@ -0,0 +1,25 @@
+<sqale>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <!-- Should be ignored -->
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>constant_resource</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_invalid_value.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_invalid_value.xml
new file mode 100644
index 00000000000..bb6bdbb4afb
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_invalid_value.xml
@@ -0,0 +1,28 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>factor</key>
+ <val>abc</val>
+ </prop>
+ <prop>
+ <key>function</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_remediation_cost_having_zero_value.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_remediation_cost_having_zero_value.xml
new file mode 100644
index 00000000000..2165a603341
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_remediation_cost_having_zero_value.xml
@@ -0,0 +1,71 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ <chc>
+ <rule-repo>squid</rule-repo>
+ <rule-key>S001</rule-key>
+ <prop>
+ <key>offset</key>
+ <val>0.0</val>
+ <txt>min</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>constant_issue</txt>
+ </prop>
+ </chc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>0.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+ <chc>
+ <key>PORTABILITY</key>
+ <name>Portability</name>
+ <chc>
+ <key>COMPILER_RELATED_PORTABILITY</key>
+ <name>Compiler related portability</name>
+ </chc>
+ <chc>
+ <key>HARDWARE_RELATED_PORTABILITY</key>
+ <name>Hardware related portability</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp2</rule-key>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear_offset</txt>
+ </prop>
+ <prop>
+ <key>remediationFactor</key>
+ <val>0.0</val>
+ <txt>d</txt>
+ </prop>
+ <prop>
+ <key>offset</key>
+ <val>0.0</val>
+ <txt>d</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_rule_on_root_characteristics.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_rule_on_root_characteristics.xml
new file mode 100644
index 00000000000..bcf3ed867d3
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_rule_on_root_characteristics.xml
@@ -0,0 +1,19 @@
+<sqale>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_badly_formatted_xml.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_badly_formatted_xml.xml
new file mode 100644
index 00000000000..6c7d153992c
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_badly_formatted_xml.xml
@@ -0,0 +1,43 @@
+<sqale>
+ <chc>
+ <key>USABILITY
+ </key>
+ <name>Usability
+ </name>
+ <desc>Estimate usability
+ </desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY
+ </key>
+ <name>Efficiency
+ </name>
+ <chc>
+ <key>MEMORY_EFFICIENCY
+ </key>
+ <name>Memory use
+ </name>
+ <chc>
+ <rule-repo>checkstyle
+ </rule-repo>
+ <rule-key>Regexp
+ </rule-key>
+ <prop>
+ <key>remediationFactor
+ </key>
+ <val>3.0
+ </val>
+ <txt>h
+ </txt>
+ </prop>
+ <prop>
+ <key>remediationFunction
+ </key>
+ <txt>linear
+ </txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_constant_issue.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_constant_issue.xml
new file mode 100644
index 00000000000..86b1f551fbe
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_constant_issue.xml
@@ -0,0 +1,29 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>offset</key>
+ <val>3.0</val>
+ <txt>d</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>constant_issue</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear.xml
new file mode 100644
index 00000000000..f641a5185ec
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear.xml
@@ -0,0 +1,29 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_having_offset_to_zero.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_having_offset_to_zero.xml
new file mode 100644
index 00000000000..12328da7c83
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_having_offset_to_zero.xml
@@ -0,0 +1,34 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ <prop>
+ <key>offset</key>
+ <val>0.0</val>
+ <txt>min</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_with_offset.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_with_offset.xml
new file mode 100644
index 00000000000..be4fdd3ce4e
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_with_offset.xml
@@ -0,0 +1,54 @@
+<!--
+ ~ SonarQube, open source software quality management tool.
+ ~ Copyright (C) 2008-2016 SonarSource
+ ~ mailto:contact AT sonarsource DOT com
+ ~
+ ~ SonarQube 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.
+ ~
+ ~ SonarQube 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.
+ -->
+
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear_offset</txt>
+ </prop>
+ <prop>
+ <key>offset</key>
+ <val>1.0</val>
+ <txt>min</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules.xml
new file mode 100644
index 00000000000..af3b906764e
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules.xml
@@ -0,0 +1,33 @@
+<sqale>
+ <chc>
+ <rule-repo>javasquid</rule-repo>
+ <rule-key>rule1</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ <chc>
+ <rule-repo>javasquid</rule-repo>
+ <rule-key>rule2</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear_offset</txt>
+ </prop>
+ <prop>
+ <key>offset</key>
+ <val>1.0</val>
+ <txt>h</txt>
+ </prop>
+ </chc>
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules_with_deprecated_quality_model_format.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules_with_deprecated_quality_model_format.xml
new file mode 100644
index 00000000000..f8f7e9b6d5d
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules_with_deprecated_quality_model_format.xml
@@ -0,0 +1,58 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>javasquid</rule-repo>
+ <rule-key>rule1</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+ <chc>
+ <key>PORTABILITY</key>
+ <name>Portability</name>
+ <chc>
+ <key>COMPILER_RELATED_PORTABILITY</key>
+ <name>Compiler related portability</name>
+ </chc>
+ <chc>
+ <key>HARDWARE_RELATED_PORTABILITY</key>
+ <name>Hardware related portability</name>
+ <chc>
+ <rule-repo>javasquid</rule-repo>
+ <rule-key>rule2</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear_offset</txt>
+ </prop>
+ <prop>
+ <key>offset</key>
+ <val>1.0</val>
+ <txt>h</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/read_integer.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/read_integer.xml
new file mode 100644
index 00000000000..483a98bebd3
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/read_integer.xml
@@ -0,0 +1,29 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3</val>
+ <txt>h</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/replace_mn_by_min.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/replace_mn_by_min.xml
new file mode 100644
index 00000000000..f6b3f2dbb96
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/replace_mn_by_min.xml
@@ -0,0 +1,49 @@
+<!--
+ ~ SonarQube, open source software quality management tool.
+ ~ Copyright (C) 2008-2016 SonarSource
+ ~ mailto:contact AT sonarsource DOT com
+ ~
+ ~ SonarQube 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.
+ ~
+ ~ SonarQube 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.
+ -->
+
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ <txt>mn</txt>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear</txt>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/use_default_unit_when_no_unit.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/use_default_unit_when_no_unit.xml
new file mode 100644
index 00000000000..34c2f2cd9e5
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/use_default_unit_when_no_unit.xml
@@ -0,0 +1,32 @@
+<sqale>
+ <chc>
+ <key>USABILITY</key>
+ <name>Usability</name>
+ <desc>Estimate usability</desc>
+ </chc>
+ <chc>
+ <key>EFFICIENCY</key>
+ <name>Efficiency</name>
+ <chc>
+ <key>MEMORY_EFFICIENCY</key>
+ <name>Memory use</name>
+ <chc>
+ <rule-repo>checkstyle</rule-repo>
+ <rule-key>Regexp</rule-key>
+ <prop>
+ <key>remediationFactor</key>
+ <val>3.0</val>
+ </prop>
+ <prop>
+ <key>remediationFunction</key>
+ <txt>linear_offset</txt>
+ </prop>
+ <prop>
+ <key>offset</key>
+ <val>1.0</val>
+ </prop>
+ </chc>
+ </chc>
+ </chc>
+
+</sqale>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/metric/DefaultMetricFinderTest/shared.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/metric/DefaultMetricFinderTest/shared.xml
new file mode 100644
index 00000000000..60a47d06c7f
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/metric/DefaultMetricFinderTest/shared.xml
@@ -0,0 +1,12 @@
+<dataset>
+
+ <metrics delete_historical_data="[null]" id="1" name="ncloc" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name=""
+ enabled="[true]" worst_value="[null]" optimized_best_value="[null]" best_value="[null]" direction="0"
+ hidden="[false]"/>
+
+ <metrics delete_historical_data="[null]" id="2" name="coverage" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name=""
+ enabled="[true]" worst_value="0" optimized_best_value="[true]" best_value="100" direction="1" hidden="[false]"/>
+
+ <metrics delete_historical_data="[null]" id="3" name="disabled" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name=""
+ enabled="[false]" worst_value="0" optimized_best_value="[true]" best_value="100" direction="1" hidden="[false]"/>
+</dataset>
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar
new file mode 100644
index 00000000000..21024e33b94
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar
Binary files differ
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/build.properties b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/build.properties
new file mode 100644
index 00000000000..230f3ae8907
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/build.properties
@@ -0,0 +1,2 @@
+Implementation-Build=0b9545a8b74aca473cb776275be4dc93a327c363
+Build-Time=1342455258749 \ No newline at end of file
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/empty-version.txt b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/empty-version.txt
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/empty-version.txt
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/version.txt b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/version.txt
new file mode 100644
index 00000000000..d3827e75a5c
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/version.txt
@@ -0,0 +1 @@
+1.0
diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/telemetry/telemetry-example.json b/server/sonar-webserver-core/src/test/resources/org/sonar/server/telemetry/telemetry-example.json
new file mode 100644
index 00000000000..5eeb14d2ff0
--- /dev/null
+++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/telemetry/telemetry-example.json
@@ -0,0 +1,55 @@
+{
+ "id": "AU-TpxcB-iU5OvuD2FL7",
+ "version": "7.5.4",
+ "edition": "developer",
+ "database": {
+ "name": "PostgreSQL",
+ "version": "9.6.5"
+ },
+ "plugins": [
+ {
+ "name": "java",
+ "version": "4.12.0.11033"
+ },
+ {
+ "name": "scmgit",
+ "version": "1.2"
+ },
+ {
+ "name": "other",
+ "version": "undefined"
+ }
+ ],
+ "userCount": 3,
+ "projectCount": 2,
+ "usingBranches": true,
+ "ncloc": 300,
+ "projectCountByLanguage": [
+ {
+ "language": "java",
+ "count": 2
+ },
+ {
+ "language": "kotlin",
+ "count": 1
+ },
+ {
+ "language": "js",
+ "count": 1
+ }
+ ],
+ "nclocByLanguage": [
+ {
+ "language": "java",
+ "ncloc": 500
+ },
+ {
+ "language": "kotlin",
+ "ncloc": 2500
+ },
+ {
+ "language": "js",
+ "ncloc": 50
+ }
+ ]
+}