aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-scanner-engine/src
diff options
context:
space:
mode:
authorJulien HENRY <julien.henry@sonarsource.com>2016-03-17 17:05:56 +0100
committerJulien HENRY <julien.henry@sonarsource.com>2016-03-18 09:35:05 +0100
commitcfcbe278f7ced12599d898d50f3fe68bfbf95155 (patch)
tree5b4116ad08a8ba87ffc5bf9f159a431b9609b48f /sonar-scanner-engine/src
parent38ce80934961773a9a35ec0401994b3d726597dc (diff)
downloadsonarqube-cfcbe278f7ced12599d898d50f3fe68bfbf95155.tar.gz
sonarqube-cfcbe278f7ced12599d898d50f3fe68bfbf95155.zip
Rename batch into scanner
Diffstat (limited to 'sonar-scanner-engine/src')
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContext.java156
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContextFactory.java52
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultProjectTree.java101
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/ProjectConfigurator.java100
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisProperties.java40
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisTempFolderProvider.java80
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisWSLoaderProvider.java49
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/DefaultAnalysisMode.java121
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/AbstractAnalysisMode.java67
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java90
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java264
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginInstaller.java159
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginJarExploder.java80
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginPredicate.java98
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginRepository.java110
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClient.java113
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClientProvider.java62
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/DroppedPropertyChecker.java48
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionInstaller.java77
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionMatcher.java30
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionUtils.java48
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/FileCacheProvider.java37
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java129
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalMode.java44
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalProperties.java36
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalSettings.java78
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalTempFolderProvider.java175
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/MetricProvider.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/PluginInstaller.java42
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/Slf4jLogger.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/UserProperties.java62
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java274
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/EnvironmentInformation.java59
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/IssueListener.java167
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogCallbackAppender.java57
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogOutput.java33
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfiguration.java153
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfigurator.java76
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/package-info.java27
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java80
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/GlobalPersistentCacheProvider.java55
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java96
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java30
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java185
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java36
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java67
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java93
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/StrategyWSLoaderProvider.java41
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoader.java240
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoaderResult.java41
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cache/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdComponents.java38
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdExecutor.java217
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdIndexer.java49
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdMappings.java52
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdSensor.java99
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DefaultCpdBlockIndexer.java114
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DuplicationPredicates.java49
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/JavaCpdBlockIndexer.java105
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/SonarCpdBlockIndex.java119
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/package-info.java24
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/package-info.java24
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java221
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/InputFileComponent.java60
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/BatchPerspectives.java73
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilder.java42
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveNotFoundException.java26
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchEvent.java45
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepEvent.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepHandler.java47
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/events/EventBus.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/events/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponent.java84
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponentCache.java72
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/Bucket.java99
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/Cache.java505
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/Caches.java100
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/CachesManager.java98
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/DefaultIndex.java353
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/index/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultFilterableIssue.java92
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssuable.java65
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueCallback.java121
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueFilterChain.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultProjectIssues.java79
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilter.java193
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueBuilderWrapper.java135
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueFilterChain.java57
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueWrapper.java193
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssuableFactory.java45
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCache.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCallback.java24
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueFilters.java69
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueTransformer.java115
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ModuleIssues.java155
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/TrackedIssueAdapter.java202
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/EnforceIssuesFilter.java67
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilter.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/AbstractPatternInitializer.java81
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializer.java105
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializer.java49
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssuePattern.java167
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/LineRange.java90
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternDecoder.java143
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternMatcher.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoader.java81
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScanner.java198
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoader.java68
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java96
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingInput.java58
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTransition.java123
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java266
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java89
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueFromWs.java65
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueRepository.java92
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueValueCoder.java45
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerLineHashesLoader.java32
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.java77
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/TrackedIssue.java262
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/FakePluginInstaller.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObserver.java32
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObservers.java44
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/TaskResult.java292
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseEvent.java41
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseExecutor.java120
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializerExecutionEvent.java50
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersExecutor.java67
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersPhaseEvent.java52
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/IssuesPhaseExecutor.java84
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PhasesTimeProfiler.java53
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobExecutionEvent.java50
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobPhaseEvent.java52
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobsExecutor.java75
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectAnalysisEvent.java50
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectInitializer.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PublishPhaseExecutor.java67
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorExecutionEvent.java50
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsExecutor.java61
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsPhaseEvent.java52
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/phases/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/platform/DefaultServer.java110
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/platform/package-info.java24
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/DefaultPostJobContext.java154
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobOptimizer.java68
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobWrapper.java65
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/package-info.java21
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/AbstractTimeProfiling.java116
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ItemProfiling.java37
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ModuleProfiling.java105
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/Phase.java37
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhaseProfiling.java98
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhasesSumUpTimeProfiler.java278
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/package-info.java24
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/ActiveRulesPublisher.java62
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/AnalysisContextReportPublisher.java164
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/ComponentsPublisher.java175
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/CoveragePublisher.java139
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/MeasuresPublisher.java168
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/MetadataPublisher.java61
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisher.java242
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisherStep.java31
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/ScannerReportUtils.java83
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/SourcePublisher.java72
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/TestExecutionAndCoveragePublisher.java139
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/report/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoader.java48
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoader.java129
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultQualityProfileLoader.java88
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java59
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/FileData.java41
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesLoader.java30
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesProvider.java45
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositories.java79
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesLoader.java28
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesProvider.java68
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileLoader.java33
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileProvider.java69
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ServerIssuesLoader.java29
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/DefaultLanguagesRepository.java78
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/Language.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/LanguagesRepository.java46
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/package-info.java21
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java117
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesLoader.java30
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java116
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultActiveRulesLoader.java136
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java66
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/LoadedActiveRule.java93
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java64
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfile.java96
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileSensor.java72
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileVerifier.java71
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RuleFinderCompatibility.java123
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesLoader.java29
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileProvider.java95
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileWrapper.java140
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProvider.java65
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/UsedQProfiles.java112
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/rule/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactor.java71
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactorProvider.java44
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/LanguageVerifier.java69
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java197
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleSettings.java96
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/MutableProjectReactorProvider.java34
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectBuildersExecutor.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectExclusions.java94
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectLock.java70
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorBuilder.java423
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorValidator.java101
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java277
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectSettings.java62
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/WorkDirectoryCleaner.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicates.java49
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ComponentIndexer.java68
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java285
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DeprecatedFileFilters.java80
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ExclusionFilters.java131
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileIndexer.java268
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileSystemLogger.java92
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilder.java111
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactory.java51
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputPathCache.java92
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetection.java143
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactory.java39
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializer.java122
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleInputFileCache.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java50
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java38
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/package-info.java26
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DefaultMetricFinder.java64
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DeprecatedMetricFinder.java80
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureCache.java84
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureValueCoder.java94
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/package-info.java26
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ConsoleReport.java143
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/HtmlReport.java179
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssueVariation.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReport.java102
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReportBuilder.java103
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReports.java38
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/JSONReport.java247
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportRuleKey.java84
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportSummary.java92
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/Reporter.java29
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ResourceReport.java155
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleNameProvider.java59
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReport.java57
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReportComparator.java64
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/SourceProvider.java63
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameInput.java46
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameOutput.java147
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmConfiguration.java155
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmSensor.java115
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/scm/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java115
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorStorage.java285
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorOptimizer.java98
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorWrapper.java64
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageConstants.java42
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageExclusions.java210
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/package-info.java21
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewCpdTokens.java43
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewHighlighting.java43
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/package-info.java21
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/package-info.java21
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizerSensor.java64
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizers.java91
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultHighlightable.java88
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbol.java66
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolTable.java123
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolizable.java113
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightableBuilder.java51
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingCodeBuilder.java91
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingRenderer.java43
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/LinesSensor.java68
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/SymbolizableBuilder.java51
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/ZeroCoverageSensor.java112
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/source/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/task/ListTask.java56
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/task/ScanTask.java72
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskContainer.java91
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskProperties.java36
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/task/Tasks.java75
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/task/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultCoverageBlock.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestCase.java163
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestPlan.java57
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestable.java86
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestPlanBuilder.java54
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestableBuilder.java47
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/test/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/util/BatchUtils.java87
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/util/ProgressReport.java79
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/batch/util/package-info.java22
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml50
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/logback.xml47
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport.ftl462
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/DIR.pngbin0 -> 390 bytes
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/FIL.pngbin0 -> 416 bytes
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/PRJ.pngbin0 -> 575 bytes
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/favicon.icobin0 -> 5430 bytes
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/jquery.min.js6
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sep12.pngbin0 -> 118 bytes
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.css396
-rwxr-xr-xsonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.eotbin0 -> 6468 bytes
-rwxr-xr-xsonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.svg33
-rwxr-xr-xsonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.ttfbin0 -> 6312 bytes
-rwxr-xr-xsonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.woffbin0 -> 6904 bytes
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonarqube-24x100.pngbin0 -> 1477 bytes
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java148
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/ProjectConfiguratorTest.java130
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisTempFolderProviderTest.java66
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java62
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/DefaultAnalysisModeTest.java141
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchExtensionDictionnaryTest.java412
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java86
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginJarExploderTest.java80
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginPredicateTest.java103
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginRepositoryTest.java76
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientProviderTest.java76
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientTest.java116
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/DroppedPropertyCheckerTest.java62
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionInstallerTest.java136
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionUtilsTest.java88
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/FileCacheProviderTest.java65
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalContainerTest.java76
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalModeTest.java89
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalPropertiesTest.java43
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalSettingsTest.java79
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalTempFolderProviderTest.java142
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MetricProviderTest.java55
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MockHttpServer.java118
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/BatchTest.java73
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/EnvironmentInformationTest.java41
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LogCallbackAppenderTest.java76
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfigurationTest.java167
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfiguratorTest.java184
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java91
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/GlobalPersistentCacheProviderTest.java83
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java93
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java197
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java80
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectSyncContainerTest.java52
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java59
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cache/WSLoaderTest.java264
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java32
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdExecutorTest.java239
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java80
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DefaultCpdBlockIndexerTest.java80
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DuplicationPredicatesTest.java38
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/JavaCpdBlockIndexerTest.java106
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilderTest.java44
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/events/BatchStepEventTest.java45
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/events/EventBusTest.java77
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/index/AbstractCachesTest.java77
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/index/BatchComponentCacheTest.java53
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/index/BucketTest.java58
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/index/CacheTest.java250
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesManagerTest.java42
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesTest.java89
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/index/DefaultIndexTest.java163
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultFilterableIssueTest.java86
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueCallbackTest.java137
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueFilterChainTest.java95
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultProjectIssuesTest.java78
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilterTest.java157
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueFilterChainTest.java96
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssuableFactoryTest.java57
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssueCacheTest.java98
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ModuleIssuesTest.java221
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/TrackedIssueAdapterTest.java84
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/EnforceIssuesFilterTest.java149
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilterTest.java67
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializerTest.java141
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializerTest.java70
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssuePatternTest.java114
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/LineRangeTest.java72
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternDecoderTest.java187
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternMatcherTest.java119
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoaderTest.java157
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest.java168
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoaderTest.java89
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java43
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java119
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/TrackedIssueTest.java47
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java497
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/LogOutputRecorder.java54
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cache/CacheSyncTest.java155
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/coverage/CoverageMediumTest.java180
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java336
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/deprecated/DeprecatedApiMediumTest.java134
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/FileSystemMediumTest.java296
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/NoLanguagesPluginsMediumTest.java76
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/ProjectBuilderMediumTest.java168
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/highlighting/HighlightingMediumTest.java133
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/ChecksMediumTest.java127
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesIssuesModeMediumTest.java100
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java187
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnDirMediumTest.java113
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnModuleMediumTest.java84
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/MultilineIssuesMediumTest.java131
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/EmptyFileTest.java94
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/IssueModeAndReportsMediumTest.java316
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NoPreviousAnalysisTest.java86
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NonAssociatedProject.java89
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/ScanOnlyChangedTest.java212
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/ExceptionHandlingMediumTest.java116
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/LogListenerTest.java216
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/measures/MeasuresMediumTest.java131
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/scm/ScmMediumTest.java363
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/symbol/SymbolMediumTest.java116
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tasks/TasksMediumTest.java160
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/CoveragePerTestMediumTest.java160
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/TestExecutionMediumTest.java105
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/phases/PostJobsExecutorTest.java60
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/platform/DefaultServerTest.java51
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/DefaultPostJobContextTest.java87
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/PostJobOptimizerTest.java77
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/profiling/PhasesSumUpTimeProfilerTest.java410
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/ActiveRulesPublisherTest.java70
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/AnalysisContextReportPublisherTest.java207
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/ComponentsPublisherTest.java170
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/CoveragePublisherTest.java116
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/MeasuresPublisherTest.java113
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/MetadataPublisherTest.java100
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/ReportPublisherTest.java164
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/report/SourcePublisherTest.java119
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoaderTest.java78
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest.java144
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultQualityProfileLoaderTest.java116
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java82
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/repository/ProjectRepositoriesProviderTest.java115
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/repository/QualityProfileProviderTest.java165
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java119
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/ActiveRulesProviderTest.java97
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultActiveRulesLoaderTest.java85
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java79
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileSensorTest.java135
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileVerifierTest.java101
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RuleFinderCompatibilityTest.java101
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProfileProviderTest.java86
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProviderTest.java66
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/rule/UsedQProfilesTest.java72
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/LanguageVerifierTest.java99
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java162
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectExclusionsTest.java122
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectLockTest.java103
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorBuilderTest.java683
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorValidatorTest.java185
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java63
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java142
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/WorkDirectoryCleanerTest.java79
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicatesTest.java45
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ComponentIndexerTest.java141
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java200
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DeprecatedFileFiltersTest.java77
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ExclusionFiltersTest.java134
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactoryTest.java49
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderTest.java118
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputPathCacheTest.java82
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactoryTest.java41
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionTest.java213
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializerTest.java92
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java35
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java52
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/measure/MeasureCacheTest.java231
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/ConsoleReportTest.java145
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/JSONReportTest.java174
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/RuleNameProviderTest.java67
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/scm/DefaultBlameOutputTest.java94
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java79
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorStorageTest.java140
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/SensorOptimizerTest.java136
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/coverage/CoverageExclusionsTest.java143
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/source/CodeColorizersTest.java230
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultHighlightableTest.java55
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolTableTest.java100
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolizableTest.java66
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/source/HighlightableBuilderTest.java58
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/source/SymbolizableBuilderTest.java59
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/task/ListTaskTest.java63
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/task/TasksTest.java90
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/util/BatchUtilsTest.java51
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/batch/util/ProgressReportTest.java90
-rw-r--r--sonar-scanner-engine/src/test/resources/logback-test.xml42
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo16
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo12
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo12
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo12
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/sonar-project.properties31
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/.sonar/sonar-report.json1
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo16
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo12
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo12
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo12
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/sonar-project.properties31
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/sonar-project.properties4
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiline.xoo9
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiple.xoo9
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Single.xoo8
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/WithFlow.xoo14
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/sonar-project.properties5
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/Empty.xoo0
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/HelloJava.xoo8
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/.gitignore1
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/sonar-project.properties6
l---------sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/testx1
l---------sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/xources1
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/sonar-project.properties6
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo11
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.measures7
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.scm11
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo8
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures2
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.scm8
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo6
-rw-r--r--sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures2
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/bootstrap/BatchPluginJarExploderTest/sonar-checkstyle-plugin-2.8.jarbin0 -> 1026947 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/api_rules_list.protobufbin0 -> 27390 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_issues.protobufbin0 -> 788 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_project.json164
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_users.protobuf0
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/cpd/ManyStatements.java11
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-mess.txt37
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-twice.txt37
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-unfinished.txt34
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-wrong-order.txt35
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp.txt35
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-no-regexp.txt33
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp-and-double-regexp.txt36
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp.txt33
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest/project.protobufbin0 -> 2246 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultQualityProfileLoaderTest/quality_profile_search_defaultbin0 -> 80 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search1.protobufbin0 -> 27828 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search2.protobufbin0 -> 14446 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultRulesLoader/response.protobuf637
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/ModuleQProfilesTest/shared.xml19
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sonar-project.properties1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sources/Fake.java5
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/newBaseDir/src/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/sonar-project.properties6
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/sonar-project.properties11
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sonar-project.properties4
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/newBaseDir/src/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/sonar-project.properties6
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/sonar-project.properties10
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/generated/any-file.properties5
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/sonar-project.properties12
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/any-file.properties2
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/sonar-project.properties12
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-unexisting-file/sonar-project.properties12
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module11/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module12/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module2/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/sonar-project.properties17
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/sonar-project.properties13
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/groovy/Fake.groovy0
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/java/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module2/src/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/sonar-project.properties19
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module2/src/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/sonar-project.properties19
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1.feature/src/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/sonar-project.properties19
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-duplicate-id/sonar-project.properties12
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/sonar-project.properties17
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/modules/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/sonar-project.properties14
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/modules/module1/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/sonar-project.properties14
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/module1/src/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/sonar-project.properties9
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-basedir/sonar-project.properties13
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/module1/src/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/sonar-project.properties8
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetFile/foo.properties4
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetList/foo.properties4
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-blank-source-dir/sonar-project.properties6
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/build/report.txt0
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sonar-project.properties8
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sources/Fake.java3
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/lib/Fake.class3
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sonar-project.properties7
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sources/Fake.java3
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-missing-source-dir/sonar-project.properties5
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sonar-project.properties7
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sonar-project.properties7
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-source-dir/sonar-project.properties6
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sonar-project.properties7
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib1.txt1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib2.txt1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sonar-project.properties7
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sources/Fake.java1
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report-without-resolved-issues.json28
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report.json64
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.java12
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.js8
-rw-r--r--sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/package.html1
625 files changed, 50990 insertions, 0 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContext.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContext.java
new file mode 100644
index 00000000000..a4045f18ce0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContext.java
@@ -0,0 +1,156 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.util.Map;
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.measures.FileLinesContext;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.PersistenceMode;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.api.utils.KeyValueFormat.Converter;
+
+public class DefaultFileLinesContext implements FileLinesContext {
+
+ private final SonarIndex index;
+ private final Resource resource;
+
+ /**
+ * metric key -> line -> value
+ */
+ private final Map<String, Map<Integer, Object>> map = Maps.newHashMap();
+
+ public DefaultFileLinesContext(SonarIndex index, Resource resource) {
+ Preconditions.checkNotNull(index);
+ Preconditions.checkArgument(ResourceUtils.isFile(resource));
+ this.index = index;
+ this.resource = resource;
+ }
+
+ @Override
+ public void setIntValue(String metricKey, int line, int value) {
+ Preconditions.checkNotNull(metricKey);
+ Preconditions.checkArgument(line > 0);
+
+ setValue(metricKey, line, value);
+ }
+
+ @Override
+ public Integer getIntValue(String metricKey, int line) {
+ Preconditions.checkNotNull(metricKey);
+ Preconditions.checkArgument(line > 0);
+
+ Map lines = map.get(metricKey);
+ if (lines == null) {
+ // not in memory, so load
+ lines = loadData(metricKey, KeyValueFormat.newIntegerConverter());
+ map.put(metricKey, lines);
+ }
+ return (Integer) lines.get(line);
+ }
+
+ @Override
+ public void setStringValue(String metricKey, int line, String value) {
+ Preconditions.checkNotNull(metricKey);
+ Preconditions.checkArgument(line > 0);
+ Preconditions.checkNotNull(value);
+
+ setValue(metricKey, line, value);
+ }
+
+ @Override
+ public String getStringValue(String metricKey, int line) {
+ Preconditions.checkNotNull(metricKey);
+ Preconditions.checkArgument(line > 0);
+
+ Map lines = map.get(metricKey);
+ if (lines == null) {
+ // not in memory, so load
+ lines = loadData(metricKey, KeyValueFormat.newStringConverter());
+ map.put(metricKey, lines);
+ }
+ return (String) lines.get(line);
+ }
+
+ private Map<Integer, Object> getOrCreateLines(String metricKey) {
+ Map<Integer, Object> lines = map.get(metricKey);
+ if (lines == null) {
+ lines = Maps.newHashMap();
+ map.put(metricKey, lines);
+ }
+ return lines;
+ }
+
+ private void setValue(String metricKey, int line, Object value) {
+ getOrCreateLines(metricKey).put(line, value);
+ }
+
+ @Override
+ public void save() {
+ for (Map.Entry<String, Map<Integer, Object>> entry : map.entrySet()) {
+ String metricKey = entry.getKey();
+ Map<Integer, Object> lines = entry.getValue();
+ if (shouldSave(lines)) {
+ String data = KeyValueFormat.format(lines);
+ Measure measure = new Measure(metricKey)
+ .setPersistenceMode(PersistenceMode.DATABASE)
+ .setData(data);
+ index.addMeasure(resource, measure);
+ entry.setValue(ImmutableMap.copyOf(lines));
+ }
+ }
+ }
+
+ private Map loadData(String metricKey, Converter converter) {
+ // FIXME no way to load measure only by key
+ Measure measure = index.getMeasure(resource, new Metric(metricKey));
+ String data = measure != null ? measure.getData() : null;
+ if (data != null) {
+ return ImmutableMap.copyOf(KeyValueFormat.parse(data, KeyValueFormat.newIntegerConverter(), converter));
+ }
+ // no such measure
+ return ImmutableMap.of();
+ }
+
+ /**
+ * Checks that measure was not saved.
+ *
+ * @see #loadData(String, Converter)
+ * @see #save()
+ */
+ private static boolean shouldSave(Map<Integer, Object> lines) {
+ return !(lines instanceof ImmutableMap);
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("map", map)
+ .toString();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContextFactory.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContextFactory.java
new file mode 100644
index 00000000000..922f101f085
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultFileLinesContextFactory.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch;
+
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.measures.FileLinesContext;
+import org.sonar.api.measures.FileLinesContextFactory;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Resource;
+
+public class DefaultFileLinesContextFactory implements FileLinesContextFactory {
+
+ private final SonarIndex index;
+
+ public DefaultFileLinesContextFactory(SonarIndex index) {
+ this.index = index;
+ }
+
+ @Override
+ public FileLinesContext createFor(Resource model) {
+ // Reload resource in case it use deprecated key
+ Resource resource = index.getResource(model);
+ return new DefaultFileLinesContext(index, resource);
+ }
+
+ @Override
+ public FileLinesContext createFor(InputFile inputFile) {
+ File sonarFile = File.create(inputFile.relativePath());
+ // Reload resource from index
+ sonarFile = index.getResource(sonarFile);
+ return new DefaultFileLinesContext(index, sonarFile);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultProjectTree.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultProjectTree.java
new file mode 100644
index 00000000000..5b56f27154c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/DefaultProjectTree.java
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang.ObjectUtils;
+import org.picocontainer.Startable;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+
+public class DefaultProjectTree implements Startable {
+
+ private final ProjectConfigurator configurator;
+ private final ImmutableProjectReactor projectReactor;
+
+ private List<Project> projects;
+ private Map<ProjectDefinition, Project> projectsByDef;
+
+ public DefaultProjectTree(ImmutableProjectReactor projectReactor, ProjectConfigurator projectConfigurator) {
+ this.projectReactor = projectReactor;
+ this.configurator = projectConfigurator;
+ }
+
+ @Override
+ public void start() {
+ doStart(projectReactor.getProjects());
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+
+ void doStart(Collection<ProjectDefinition> definitions) {
+ projects = Lists.newArrayList();
+ projectsByDef = Maps.newHashMap();
+
+ for (ProjectDefinition def : definitions) {
+ Project project = configurator.create(def);
+ projectsByDef.put(def, project);
+ projects.add(project);
+ }
+
+ for (Map.Entry<ProjectDefinition, Project> entry : projectsByDef.entrySet()) {
+ ProjectDefinition def = entry.getKey();
+ Project project = entry.getValue();
+ for (ProjectDefinition module : def.getSubProjects()) {
+ projectsByDef.get(module).setParent(project);
+ }
+ }
+
+ // Configure
+ for (Project project : projects) {
+ configurator.configure(project);
+ }
+ }
+
+ public List<Project> getProjects() {
+ return projects;
+ }
+
+ public Project getRootProject() {
+ for (Project project : projects) {
+ if (project.getParent() == null) {
+ return project;
+ }
+ }
+ throw new IllegalStateException("Can not find the root project from the list of Maven modules");
+ }
+
+ public ProjectDefinition getProjectDefinition(Project project) {
+ for (Map.Entry<ProjectDefinition, Project> entry : projectsByDef.entrySet()) {
+ if (ObjectUtils.equals(entry.getValue(), project)) {
+ return entry.getKey();
+ }
+ }
+ throw new IllegalStateException("Can not find ProjectDefinition for " + project);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/ProjectConfigurator.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/ProjectConfigurator.java
new file mode 100644
index 00000000000..997cae744f2
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/ProjectConfigurator.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch;
+
+import java.util.Date;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.SonarException;
+import org.sonar.api.utils.System2;
+
+/**
+ * Used by views !!
+ *
+ */
+@BatchSide
+public class ProjectConfigurator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ProjectConfigurator.class);
+ private final System2 system2;
+ private Settings settings;
+
+ public ProjectConfigurator(Settings settings, System2 system2) {
+ this.settings = settings;
+ this.system2 = system2;
+ }
+
+ public Project create(ProjectDefinition definition) {
+ Project project = new Project(definition.getKey(), definition.getBranch(), definition.getName());
+ project.setDescription(StringUtils.defaultString(definition.getDescription()));
+ return project;
+ }
+
+ public ProjectConfigurator configure(Project project) {
+ Date analysisDate = loadAnalysisDate();
+ project
+ .setAnalysisDate(analysisDate)
+ .setAnalysisVersion(loadAnalysisVersion())
+ .setAnalysisType(loadAnalysisType());
+ return this;
+ }
+
+ private Date loadAnalysisDate() {
+ Date date;
+ try {
+ // sonar.projectDate may have been specified as a time
+ date = settings.getDateTime(CoreProperties.PROJECT_DATE_PROPERTY);
+ } catch (SonarException e) {
+ // this is probably just a date
+ date = settings.getDate(CoreProperties.PROJECT_DATE_PROPERTY);
+ }
+ if (date == null) {
+ date = new Date(system2.now());
+ settings.setProperty(CoreProperties.PROJECT_DATE_PROPERTY, date, true);
+ }
+ return date;
+ }
+
+ private Project.AnalysisType loadAnalysisType() {
+ String value = settings.getString(CoreProperties.DYNAMIC_ANALYSIS_PROPERTY);
+ if (value == null) {
+ return Project.AnalysisType.DYNAMIC;
+ }
+
+ LOG.warn("'sonar.dynamicAnalysis' is deprecated since version 4.3 and should no longer be used.");
+ if ("true".equals(value)) {
+ return Project.AnalysisType.DYNAMIC;
+ }
+ if ("reuseReports".equals(value)) {
+ return Project.AnalysisType.REUSE_REPORTS;
+ }
+ return Project.AnalysisType.STATIC;
+ }
+
+ private String loadAnalysisVersion() {
+ return settings.getString(CoreProperties.PROJECT_VERSION_PROPERTY);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisProperties.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisProperties.java
new file mode 100644
index 00000000000..e3be61067e6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisProperties.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.analysis;
+
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.batch.bootstrap.UserProperties;
+
+/**
+ * Batch properties that are specific to an analysis (for example
+ * coming from sonar-project.properties).
+ */
+public class AnalysisProperties extends UserProperties {
+ public AnalysisProperties(Map<String, String> properties) {
+ this(properties, null);
+
+ }
+
+ public AnalysisProperties(Map<String, String> properties, @Nullable String pathToSecretKey) {
+ super(properties, pathToSecretKey);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisTempFolderProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisTempFolderProvider.java
new file mode 100644
index 00000000000..68c2608b864
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisTempFolderProvider.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.analysis;
+
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.picocontainer.PicoContainer;
+import org.picocontainer.ComponentLifecycle;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.utils.TempFolder;
+import org.sonar.api.utils.internal.DefaultTempFolder;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class AnalysisTempFolderProvider extends ProviderAdapter implements ComponentLifecycle<TempFolder> {
+ static final String TMP_NAME = ".sonartmp";
+ private DefaultTempFolder projectTempFolder;
+ private boolean started = false;
+
+ public TempFolder provide(ProjectReactor projectReactor) {
+ if (projectTempFolder == null) {
+ Path workingDir = projectReactor.getRoot().getWorkDir().toPath();
+ Path tempDir = workingDir.normalize().resolve(TMP_NAME);
+ try {
+ Files.deleteIfExists(tempDir);
+ Files.createDirectories(tempDir);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to create root temp directory " + tempDir, e);
+ }
+
+ projectTempFolder = new DefaultTempFolder(tempDir.toFile(), true);
+ }
+ return projectTempFolder;
+ }
+
+ @Override
+ public void start(PicoContainer container) {
+ started = true;
+ }
+
+ @Override
+ public void stop(PicoContainer container) {
+ if (projectTempFolder != null) {
+ projectTempFolder.stop();
+ }
+ }
+
+ @Override
+ public void dispose(PicoContainer container) {
+ //nothing to do
+ }
+
+ @Override
+ public boolean componentHasLifecycle() {
+ return true;
+ }
+
+ @Override
+ public boolean isStarted() {
+ return started;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisWSLoaderProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisWSLoaderProvider.java
new file mode 100644
index 00000000000..54ce6e112a3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/AnalysisWSLoaderProvider.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.analysis;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.home.cache.PersistentCache;
+
+public class AnalysisWSLoaderProvider extends ProviderAdapter {
+ static final String SONAR_USE_WS_CACHE = "sonar.useWsCache";
+ private WSLoader wsLoader;
+
+ public WSLoader provide(AnalysisMode mode, PersistentCache cache, BatchWsClient client, AnalysisProperties props) {
+ if (wsLoader == null) {
+ // recreate cache directory if needed for this analysis
+ cache.reconfigure();
+ wsLoader = new WSLoader(getStrategy(mode, props), cache, client);
+ }
+ return wsLoader;
+ }
+
+ private static LoadStrategy getStrategy(AnalysisMode mode, AnalysisProperties props) {
+ if (mode.isIssues() && "true".equals(props.property(SONAR_USE_WS_CACHE))) {
+ return LoadStrategy.CACHE_ONLY;
+ }
+
+ return LoadStrategy.SERVER_ONLY;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/DefaultAnalysisMode.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/DefaultAnalysisMode.java
new file mode 100644
index 00000000000..bd27cf81ca0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/DefaultAnalysisMode.java
@@ -0,0 +1,121 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.analysis;
+
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.batch.bootstrap.AbstractAnalysisMode;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.mediumtest.FakePluginInstaller;
+
+/**
+ * @since 4.0
+ */
+public class DefaultAnalysisMode extends AbstractAnalysisMode implements AnalysisMode {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultAnalysisMode.class);
+ private static final String KEY_SCAN_ALL = "sonar.scanAllFiles";
+
+ private boolean mediumTestMode;
+ private boolean notAssociated;
+ private boolean scanAllFiles;
+
+ public DefaultAnalysisMode(GlobalProperties globalProps, AnalysisProperties props) {
+ init(globalProps.properties(), props.properties());
+ }
+
+ public boolean isMediumTest() {
+ return mediumTestMode;
+ }
+
+ public boolean isNotAssociated() {
+ return notAssociated;
+ }
+
+ public boolean scanAllFiles() {
+ return scanAllFiles;
+ }
+
+ private void init(Map<String, String> globalProps, Map<String, String> analysisProps) {
+ // make sure analysis is consistent with global properties
+ boolean globalPreview = isIssues(globalProps);
+ boolean analysisPreview = isIssues(analysisProps);
+
+ if (!globalPreview && analysisPreview) {
+ throw new IllegalStateException("Inconsistent properties: global properties doesn't enable issues mode while analysis properties enables it");
+ }
+
+ load(globalProps, analysisProps);
+ }
+
+ private void load(Map<String, String> globalProps, Map<String, String> analysisProps) {
+ String mode = getPropertyWithFallback(analysisProps, globalProps, CoreProperties.ANALYSIS_MODE);
+ validate(mode);
+ issues = CoreProperties.ANALYSIS_MODE_ISSUES.equals(mode) || CoreProperties.ANALYSIS_MODE_PREVIEW.equals(mode);
+ mediumTestMode = "true".equals(getPropertyWithFallback(analysisProps, globalProps, FakePluginInstaller.MEDIUM_TEST_ENABLED));
+ notAssociated = issues && rootProjectKeyMissing(analysisProps);
+ String scanAllStr = getPropertyWithFallback(analysisProps, globalProps, KEY_SCAN_ALL);
+ scanAllFiles = !issues || "true".equals(scanAllStr);
+ }
+
+ public void printMode() {
+ if (preview) {
+ LOG.info("Preview mode");
+ } else if (issues) {
+ LOG.info("Issues mode");
+ } else {
+ LOG.info("Publish mode");
+ }
+ if (mediumTestMode) {
+ LOG.info("Medium test mode");
+ }
+ if (notAssociated) {
+ LOG.info("Local analysis");
+ }
+ if (!scanAllFiles) {
+ LOG.info("Scanning only changed files");
+ }
+ }
+
+ @CheckForNull
+ private static String getPropertyWithFallback(Map<String, String> props1, Map<String, String> props2, String key) {
+ if (props1.containsKey(key)) {
+ return props1.get(key);
+ }
+
+ return props2.get(key);
+ }
+
+ private static boolean isIssues(Map<String, String> props) {
+ String mode = props.get(CoreProperties.ANALYSIS_MODE);
+
+ return CoreProperties.ANALYSIS_MODE_ISSUES.equals(mode);
+ }
+
+ private static boolean rootProjectKeyMissing(Map<String, String> props) {
+ // ProjectReactorBuilder depends on this class, so it will only create this property later
+ return !props.containsKey(CoreProperties.PROJECT_KEY_PROPERTY);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/package-info.java
new file mode 100644
index 00000000000..be39545404c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/analysis/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.analysis;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/AbstractAnalysisMode.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/AbstractAnalysisMode.java
new file mode 100644
index 00000000000..2944accbd27
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/AbstractAnalysisMode.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+
+import java.util.Arrays;
+
+import org.sonar.api.batch.AnalysisMode;
+
+public abstract class AbstractAnalysisMode implements AnalysisMode {
+ private static final String[] VALID_MODES = {CoreProperties.ANALYSIS_MODE_PREVIEW, CoreProperties.ANALYSIS_MODE_PUBLISH, CoreProperties.ANALYSIS_MODE_ISSUES};
+
+ protected boolean preview;
+ protected boolean issues;
+
+ protected AbstractAnalysisMode() {
+ }
+
+ @Override
+ public boolean isPreview() {
+ return preview;
+ }
+
+ @Override
+ public boolean isIssues() {
+ return issues;
+ }
+
+ @Override
+ public boolean isPublish() {
+ return !preview && !issues;
+ }
+
+ protected static void validate(String mode) {
+ if (StringUtils.isEmpty(mode)) {
+ return;
+ }
+
+ if (CoreProperties.ANALYSIS_MODE_INCREMENTAL.equals(mode)) {
+ throw new IllegalStateException("Invalid analysis mode: " + mode + ". This mode was removed in SonarQube 5.2. Valid modes are: " + Arrays.toString(VALID_MODES));
+ }
+
+ if (!Arrays.asList(VALID_MODES).contains(mode)) {
+ throw new IllegalStateException("Invalid analysis mode: " + mode + ". Valid modes are: " + Arrays.toString(VALID_MODES));
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java
new file mode 100644
index 00000000000..0a2ec661d73
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import java.util.List;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.batch.cpd.CpdComponents;
+import org.sonar.batch.issue.tracking.ServerIssueFromWs;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import org.sonar.batch.scan.report.ConsoleReport;
+import org.sonar.batch.scan.report.HtmlReport;
+import org.sonar.batch.scan.report.IssuesReportBuilder;
+import org.sonar.batch.scan.report.JSONReport;
+import org.sonar.batch.scan.report.RuleNameProvider;
+import org.sonar.batch.scan.report.SourceProvider;
+import org.sonar.batch.scm.ScmConfiguration;
+import org.sonar.batch.scm.ScmSensor;
+import org.sonar.batch.source.CodeColorizerSensor;
+import org.sonar.batch.source.LinesSensor;
+import org.sonar.batch.source.ZeroCoverageSensor;
+import org.sonar.batch.task.ListTask;
+import org.sonar.batch.task.ScanTask;
+import org.sonar.batch.task.Tasks;
+import org.sonar.core.component.DefaultResourceTypes;
+import org.sonar.core.config.CorePropertyDefinitions;
+import org.sonar.core.issue.tracking.Tracker;
+import org.sonar.core.platform.SonarQubeVersionProvider;
+
+public class BatchComponents {
+ private BatchComponents() {
+ // only static stuff
+ }
+
+ public static Collection<Object> all(AnalysisMode analysisMode) {
+ List<Object> components = Lists.newArrayList(
+ new SonarQubeVersionProvider(),
+ DefaultResourceTypes.get(),
+
+ // Tasks
+ Tasks.class,
+ ListTask.DEFINITION,
+ ListTask.class,
+ ScanTask.DEFINITION,
+ ScanTask.class);
+ components.addAll(CorePropertyDefinitions.all());
+ if (!analysisMode.isIssues()) {
+ // SCM
+ components.add(ScmConfiguration.class);
+ components.add(ScmSensor.class);
+
+ components.add(LinesSensor.class);
+ components.add(ZeroCoverageSensor.class);
+ components.add(CodeColorizerSensor.class);
+
+ // CPD
+ components.addAll(CpdComponents.all());
+ } else {
+ // Issues tracking
+ components.add(new Tracker<TrackedIssue, ServerIssueFromWs>());
+
+ // Issues report
+ components.add(ConsoleReport.class);
+ components.add(JSONReport.class);
+ components.add(HtmlReport.class);
+ components.add(IssuesReportBuilder.class);
+ components.add(SourceProvider.class);
+ components.add(RuleNameProvider.class);
+ }
+ return components;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java
new file mode 100644
index 00000000000..cfebad6cbb5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchExtensionDictionnary.java
@@ -0,0 +1,264 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.ClassUtils;
+import org.sonar.api.batch.CheckProject;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.batch.Phase;
+import org.sonar.api.batch.postjob.PostJob;
+import org.sonar.api.batch.postjob.PostJobContext;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.AnnotationUtils;
+import org.sonar.api.utils.dag.DirectAcyclicGraph;
+import org.sonar.batch.postjob.PostJobOptimizer;
+import org.sonar.batch.postjob.PostJobWrapper;
+import org.sonar.batch.sensor.DefaultSensorContext;
+import org.sonar.batch.sensor.SensorOptimizer;
+import org.sonar.batch.sensor.SensorWrapper;
+import org.sonar.core.platform.ComponentContainer;
+
+/**
+ * @since 2.6
+ */
+public class BatchExtensionDictionnary {
+
+ private final ComponentContainer componentContainer;
+ private final SensorContext sensorContext;
+ private final SensorOptimizer sensorOptimizer;
+ private final PostJobContext postJobContext;
+ private final PostJobOptimizer postJobOptimizer;
+
+ public BatchExtensionDictionnary(ComponentContainer componentContainer, DefaultSensorContext sensorContext, SensorOptimizer sensorOptimizer, PostJobContext postJobContext,
+ PostJobOptimizer postJobOptimizer) {
+ this.componentContainer = componentContainer;
+ this.sensorContext = sensorContext;
+ this.sensorOptimizer = sensorOptimizer;
+ this.postJobContext = postJobContext;
+ this.postJobOptimizer = postJobOptimizer;
+ }
+
+ public <T> Collection<T> select(Class<T> type, @Nullable Project project, boolean sort, @Nullable ExtensionMatcher matcher) {
+ List<T> result = getFilteredExtensions(type, project, matcher);
+ if (sort) {
+ return sort(result);
+ }
+ return result;
+ }
+
+ private static Phase.Name evaluatePhase(Object extension) {
+ Object extensionToEvaluate;
+ if (extension instanceof SensorWrapper) {
+ extensionToEvaluate = ((SensorWrapper) extension).wrappedSensor();
+ } else {
+ extensionToEvaluate = extension;
+ }
+ Phase phaseAnnotation = AnnotationUtils.getAnnotation(extensionToEvaluate, Phase.class);
+ if (phaseAnnotation != null) {
+ return phaseAnnotation.name();
+ }
+ return Phase.Name.DEFAULT;
+ }
+
+ private <T> List<T> getFilteredExtensions(Class<T> type, @Nullable Project project, @Nullable ExtensionMatcher matcher) {
+ List<T> result = Lists.newArrayList();
+ for (Object extension : getExtensions(type)) {
+ if (org.sonar.api.batch.Sensor.class.equals(type) && extension instanceof Sensor) {
+ extension = new SensorWrapper((Sensor) extension, sensorContext, sensorOptimizer);
+ }
+ if (shouldKeep(type, extension, project, matcher)) {
+ result.add((T) extension);
+ }
+ }
+ if (org.sonar.api.batch.Sensor.class.equals(type)) {
+ // Retrieve new Sensors and wrap then in SensorWrapper
+ for (Object extension : getExtensions(Sensor.class)) {
+ extension = new SensorWrapper((Sensor) extension, sensorContext, sensorOptimizer);
+ if (shouldKeep(type, extension, project, matcher)) {
+ result.add((T) extension);
+ }
+ }
+ }
+ if (org.sonar.api.batch.PostJob.class.equals(type)) {
+ // Retrieve new PostJob and wrap then in PostJobWrapper
+ for (Object extension : getExtensions(PostJob.class)) {
+ extension = new PostJobWrapper((PostJob) extension, postJobContext, postJobOptimizer);
+ if (shouldKeep(type, extension, project, matcher)) {
+ result.add((T) extension);
+ }
+ }
+ }
+ return result;
+ }
+
+ protected List<Object> getExtensions(Class type) {
+ List<Object> extensions = Lists.newArrayList();
+ completeBatchExtensions(componentContainer, extensions, type);
+ return extensions;
+ }
+
+ private static void completeBatchExtensions(ComponentContainer container, List<Object> extensions, Class type) {
+ if (container != null) {
+ extensions.addAll(container.getComponentsByType(type));
+ completeBatchExtensions(container.getParent(), extensions, type);
+ }
+ }
+
+ public <T> Collection<T> sort(Collection<T> extensions) {
+ DirectAcyclicGraph dag = new DirectAcyclicGraph();
+
+ for (T extension : extensions) {
+ dag.add(extension);
+ for (Object dependency : getDependencies(extension)) {
+ dag.add(extension, dependency);
+ }
+ for (Object generates : getDependents(extension)) {
+ dag.add(generates, extension);
+ }
+ completePhaseDependencies(dag, extension);
+ }
+ List sortedList = dag.sort();
+
+ return Collections2.filter(sortedList, Predicates.in(extensions));
+ }
+
+ /**
+ * Extension dependencies
+ */
+ private <T> List<Object> getDependencies(T extension) {
+ List<Object> result = new ArrayList<>();
+ result.addAll(evaluateAnnotatedClasses(extension, DependsUpon.class));
+ return result;
+ }
+
+ /**
+ * Objects that depend upon this extension.
+ */
+ public <T> List<Object> getDependents(T extension) {
+ List<Object> result = new ArrayList<>();
+ result.addAll(evaluateAnnotatedClasses(extension, DependedUpon.class));
+ return result;
+ }
+
+ private static void completePhaseDependencies(DirectAcyclicGraph dag, Object extension) {
+ Phase.Name phase = evaluatePhase(extension);
+ dag.add(extension, phase);
+ for (Phase.Name name : Phase.Name.values()) {
+ if (phase.compareTo(name) < 0) {
+ dag.add(name, extension);
+ } else if (phase.compareTo(name) > 0) {
+ dag.add(extension, name);
+ }
+ }
+ }
+
+ protected List<Object> evaluateAnnotatedClasses(Object extension, Class<? extends Annotation> annotation) {
+ List<Object> results = Lists.newArrayList();
+ Class aClass = extension.getClass();
+ while (aClass != null) {
+ evaluateClass(aClass, annotation, results);
+
+ for (Method method : aClass.getDeclaredMethods()) {
+ if (method.getAnnotation(annotation) != null) {
+ checkAnnotatedMethod(method);
+ evaluateMethod(extension, method, results);
+ }
+ }
+ aClass = aClass.getSuperclass();
+ }
+
+ return results;
+ }
+
+ private static void evaluateClass(Class extensionClass, Class annotationClass, List<Object> results) {
+ Annotation annotation = extensionClass.getAnnotation(annotationClass);
+ if (annotation != null) {
+ if (annotation.annotationType().isAssignableFrom(DependsUpon.class)) {
+ results.addAll(Arrays.asList(((DependsUpon) annotation).value()));
+
+ } else if (annotation.annotationType().isAssignableFrom(DependedUpon.class)) {
+ results.addAll(Arrays.asList(((DependedUpon) annotation).value()));
+ }
+ }
+
+ Class[] interfaces = extensionClass.getInterfaces();
+ for (Class anInterface : interfaces) {
+ evaluateClass(anInterface, annotationClass, results);
+ }
+ }
+
+ private void evaluateMethod(Object extension, Method method, List<Object> results) {
+ try {
+ Object result = method.invoke(extension);
+ if (result != null) {
+ if (result instanceof Class<?>) {
+ results.addAll(componentContainer.getComponentsByType((Class<?>) result));
+
+ } else if (result instanceof Collection<?>) {
+ results.addAll((Collection<?>) result);
+
+ } else if (result.getClass().isArray()) {
+ for (int i = 0; i < Array.getLength(result); i++) {
+ results.add(Array.get(result, i));
+ }
+
+ } else {
+ results.add(result);
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Can not invoke method " + method, e);
+ }
+ }
+
+ private static void checkAnnotatedMethod(Method method) {
+ if (!Modifier.isPublic(method.getModifiers())) {
+ throw new IllegalStateException("Annotated method must be public:" + method);
+ }
+ if (method.getParameterTypes().length > 0) {
+ throw new IllegalStateException("Annotated method must not have parameters:" + method);
+ }
+ }
+
+ private static boolean shouldKeep(Class type, Object extension, @Nullable Project project, @Nullable ExtensionMatcher matcher) {
+ boolean keep = (ClassUtils.isAssignable(extension.getClass(), type)
+ || (org.sonar.api.batch.Sensor.class.equals(type) && ClassUtils.isAssignable(extension.getClass(), Sensor.class)))
+ && (matcher == null || matcher.accept(extension));
+ if (keep && project != null && ClassUtils.isAssignable(extension.getClass(), CheckProject.class)) {
+ keep = ((CheckProject) extension).shouldExecuteOnProject(project);
+ }
+ return keep;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginInstaller.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginInstaller.java
new file mode 100644
index 00000000000..1c1ee8e1fc4
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginInstaller.java
@@ -0,0 +1,159 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.CharUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.SonarPlugin;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.RemotePlugin;
+import org.sonar.core.platform.RemotePluginFile;
+import org.sonar.home.cache.FileCache;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static java.lang.String.format;
+
+/**
+ * Downloads the plugins installed on server and stores them in a local user cache
+ * (see {@link FileCacheProvider}).
+ */
+public class BatchPluginInstaller implements PluginInstaller {
+
+ private static final Logger LOG = Loggers.get(BatchPluginInstaller.class);
+ private static final String PLUGINS_INDEX_URL = "/deploy/plugins/index.txt";
+
+ private final WSLoader wsLoader;
+ private final FileCache fileCache;
+ private final BatchPluginPredicate pluginPredicate;
+ private final BatchWsClient wsClient;
+
+ public BatchPluginInstaller(WSLoader wsLoader, BatchWsClient wsClient, FileCache fileCache, BatchPluginPredicate pluginPredicate) {
+ this.wsLoader = wsLoader;
+ this.fileCache = fileCache;
+ this.pluginPredicate = pluginPredicate;
+ this.wsClient = wsClient;
+ }
+
+ @Override
+ public Map<String, PluginInfo> installRemotes() {
+ return loadPlugins(listRemotePlugins());
+ }
+
+ private Map<String, PluginInfo> loadPlugins(List<RemotePlugin> remotePlugins) {
+ Map<String, PluginInfo> infosByKey = new HashMap<>();
+
+ Profiler profiler = Profiler.create(LOG).startDebug("Load plugins");
+
+ for (RemotePlugin remotePlugin : remotePlugins) {
+ if (pluginPredicate.apply(remotePlugin.getKey())) {
+ File jarFile = download(remotePlugin);
+ PluginInfo info = PluginInfo.create(jarFile);
+ infosByKey.put(info.getKey(), info);
+ }
+ }
+
+ profiler.stopDebug();
+ return infosByKey;
+ }
+
+ /**
+ * Returns empty on purpose. This method is used only by tests.
+ * @see org.sonar.batch.mediumtest.BatchMediumTester
+ */
+ @Override
+ public Map<String, SonarPlugin> installLocals() {
+ return Collections.emptyMap();
+ }
+
+ @VisibleForTesting
+ File download(final RemotePlugin remote) {
+ try {
+ final RemotePluginFile file = remote.file();
+ return fileCache.get(file.getFilename(), file.getHash(), new FileDownloader(remote.getKey()));
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to download plugin: " + remote.getKey(), e);
+ }
+ }
+
+ /**
+ * Gets information about the plugins installed on server (filename, checksum)
+ */
+ @VisibleForTesting
+ List<RemotePlugin> listRemotePlugins() {
+ try {
+ String pluginIndex = loadPluginIndex();
+ String[] rows = StringUtils.split(pluginIndex, CharUtils.LF);
+ List<RemotePlugin> result = Lists.newArrayList();
+ for (String row : rows) {
+ result.add(RemotePlugin.unmarshal(row));
+ }
+ return result;
+
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to load plugin index: " + PLUGINS_INDEX_URL, e);
+ }
+ }
+
+ private String loadPluginIndex() {
+ Profiler profiler = Profiler.create(LOG).startInfo("Load plugins index");
+ WSLoaderResult<String> wsResult = wsLoader.loadString(PLUGINS_INDEX_URL);
+ profiler.stopInfo(wsResult.isFromCache());
+ return wsResult.get();
+ }
+
+ private class FileDownloader implements FileCache.Downloader {
+ private String key;
+
+ FileDownloader(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public void download(String filename, File toFile) throws IOException {
+ String url = format("/deploy/plugins/%s/%s", key, filename);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Download plugin {} to {}", filename, toFile);
+ } else {
+ LOG.info("Download {}", filename);
+ }
+
+ WsResponse response = wsClient.call(new GetRequest(url));
+ try (InputStream stream = response.contentStream()) {
+ FileUtils.copyInputStreamToFile(stream, toFile);
+ }
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginJarExploder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginJarExploder.java
new file mode 100644
index 00000000000..a91a6b38cbc
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginJarExploder.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.apache.commons.io.FileUtils;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.utils.ZipUtils;
+import org.sonar.core.platform.ExplodedPlugin;
+import org.sonar.core.platform.PluginJarExploder;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.home.cache.FileCache;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import static org.sonar.core.util.FileUtils.deleteQuietly;
+
+@BatchSide
+public class BatchPluginJarExploder extends PluginJarExploder {
+
+ private final FileCache fileCache;
+
+ public BatchPluginJarExploder(FileCache fileCache) {
+ this.fileCache = fileCache;
+ }
+
+ @Override
+ public ExplodedPlugin explode(PluginInfo info) {
+ try {
+ File dir = unzipFile(info.getNonNullJarFile());
+ return explodeFromUnzippedDir(info.getKey(), info.getNonNullJarFile(), dir);
+ } catch (Exception e) {
+ throw new IllegalStateException(String.format("Fail to open plugin [%s]: %s", info.getKey(), info.getNonNullJarFile().getAbsolutePath()), e);
+ }
+ }
+
+ private File unzipFile(File cachedFile) throws IOException {
+ String filename = cachedFile.getName();
+ File destDir = new File(cachedFile.getParentFile(), filename + "_unzip");
+ File lockFile = new File(cachedFile.getParentFile(), filename + "_unzip.lock");
+ if (!destDir.exists()) {
+ FileOutputStream out = new FileOutputStream(lockFile);
+ try {
+ java.nio.channels.FileLock lock = out.getChannel().lock();
+ try {
+ // Recheck in case of concurrent processes
+ if (!destDir.exists()) {
+ File tempDir = fileCache.createTempDir();
+ ZipUtils.unzip(cachedFile, tempDir, newLibFilter());
+ FileUtils.moveDirectory(tempDir, destDir);
+ }
+ } finally {
+ lock.release();
+ }
+ } finally {
+ out.close();
+ deleteQuietly(lockFile);
+ }
+ }
+ return destDir;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginPredicate.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginPredicate.java
new file mode 100644
index 00000000000..ea4380c44da
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginPredicate.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import static com.google.common.collect.Sets.newHashSet;
+
+/**
+ * Filters the plugins to be enabled during analysis
+ */
+@BatchSide
+public class BatchPluginPredicate implements Predicate<String> {
+
+ private static final Logger LOG = Loggers.get(BatchPluginPredicate.class);
+
+ private static final String BUILDBREAKER_PLUGIN_KEY = "buildbreaker";
+ private static final Joiner COMMA_JOINER = Joiner.on(", ");
+
+ private final Set<String> whites = newHashSet();
+ private final Set<String> blacks = newHashSet();
+ private final GlobalMode mode;
+
+ public BatchPluginPredicate(Settings settings, GlobalMode mode) {
+ this.mode = mode;
+ if (mode.isPreview() || mode.isIssues()) {
+ // These default values are not supported by Settings because the class CorePlugin
+ // is not loaded yet.
+ whites.addAll(propertyValues(settings,
+ CoreProperties.PREVIEW_INCLUDE_PLUGINS, CoreProperties.PREVIEW_INCLUDE_PLUGINS_DEFAULT_VALUE));
+ blacks.addAll(propertyValues(settings,
+ CoreProperties.PREVIEW_EXCLUDE_PLUGINS, CoreProperties.PREVIEW_EXCLUDE_PLUGINS_DEFAULT_VALUE));
+ }
+ if (!whites.isEmpty()) {
+ LOG.info("Include plugins: " + COMMA_JOINER.join(whites));
+ }
+ if (!blacks.isEmpty()) {
+ LOG.info("Exclude plugins: " + COMMA_JOINER.join(blacks));
+ }
+ }
+
+ @Override
+ public boolean apply(@Nonnull String pluginKey) {
+ if (BUILDBREAKER_PLUGIN_KEY.equals(pluginKey) && mode.isPreview()) {
+ LOG.info("Build Breaker plugin is no more supported in preview mode");
+ return false;
+ }
+
+ if (whites.isEmpty()) {
+ return blacks.isEmpty() || !blacks.contains(pluginKey);
+ }
+ return whites.contains(pluginKey);
+ }
+
+ Set<String> getWhites() {
+ return whites;
+ }
+
+ Set<String> getBlacks() {
+ return blacks;
+ }
+
+ private static List<String> propertyValues(Settings settings, String key, String defaultValue) {
+ String s = StringUtils.defaultIfEmpty(settings.getString(key), defaultValue);
+ return Lists.newArrayList(Splitter.on(",").trimResults().omitEmptyStrings().split(s));
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginRepository.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginRepository.java
new file mode 100644
index 00000000000..4d0d0c46c2d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchPluginRepository.java
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import java.util.Collection;
+import java.util.Map;
+import org.picocontainer.Startable;
+import org.sonar.api.SonarPlugin;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginLoader;
+import org.sonar.core.platform.PluginRepository;
+
+/**
+ * Orchestrates the installation and loading of plugins
+ */
+public class BatchPluginRepository implements PluginRepository, Startable {
+ private static final Logger LOG = Loggers.get(BatchPluginRepository.class);
+
+ private final PluginInstaller installer;
+ private final PluginLoader loader;
+
+ private Map<String, SonarPlugin> pluginInstancesByKeys;
+ private Map<String, PluginInfo> infosByKeys;
+
+ public BatchPluginRepository(PluginInstaller installer, PluginLoader loader) {
+ this.installer = installer;
+ this.loader = loader;
+ }
+
+ @Override
+ public void start() {
+ infosByKeys = Maps.newHashMap(installer.installRemotes());
+ pluginInstancesByKeys = Maps.newHashMap(loader.load(infosByKeys));
+
+ // this part is only used by tests
+ for (Map.Entry<String, SonarPlugin> entry : installer.installLocals().entrySet()) {
+ String pluginKey = entry.getKey();
+ infosByKeys.put(pluginKey, new PluginInfo(pluginKey));
+ pluginInstancesByKeys.put(pluginKey, entry.getValue());
+ }
+
+ logPlugins();
+ }
+
+ private void logPlugins() {
+ if (infosByKeys.isEmpty()) {
+ LOG.debug("No plugins loaded");
+ } else {
+ LOG.debug("Plugins:");
+ for (PluginInfo p : infosByKeys.values()) {
+ LOG.debug(" * {} {} ({})", p.getName(), p.getVersion(), p.getKey());
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ // close plugin classloaders
+ loader.unload(pluginInstancesByKeys.values());
+
+ pluginInstancesByKeys.clear();
+ infosByKeys.clear();
+ }
+
+ @Override
+ public Collection<PluginInfo> getPluginInfos() {
+ return infosByKeys.values();
+ }
+
+ @Override
+ public PluginInfo getPluginInfo(String key) {
+ PluginInfo info = infosByKeys.get(key);
+ Preconditions.checkState(info != null, String.format("Plugin [%s] does not exist", key));
+ return info;
+ }
+
+ @Override
+ public SonarPlugin getPluginInstance(String key) {
+ SonarPlugin instance = pluginInstancesByKeys.get(key);
+ Preconditions.checkState(instance != null, String.format("Plugin [%s] does not exist", key));
+ return instance;
+ }
+
+ @Override
+ public boolean hasPlugin(String key) {
+ return infosByKeys.containsKey(key);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClient.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClient.java
new file mode 100644
index 00000000000..d77670a6c86
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClient.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsConnector;
+import org.sonarqube.ws.client.WsRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static java.lang.String.format;
+
+public class BatchWsClient {
+
+ private static final Logger LOG = Loggers.get(BatchWsClient.class);
+
+ private final WsClient target;
+ private final boolean hasCredentials;
+
+ public BatchWsClient(WsClient target, boolean hasCredentials) {
+ this.target = target;
+ this.hasCredentials = hasCredentials;
+ }
+
+ /**
+ * @throws IllegalStateException if the request could not be executed due to
+ * a connectivity problem or timeout. Because networks can
+ * fail during an exchange, it is possible that the remote server
+ * accepted the request before the failure
+ * @throws HttpException if the response code is not in range [200..300)
+ */
+ public WsResponse call(WsRequest request) {
+ Profiler profiler = Profiler.createIfDebug(LOG).start();
+ WsResponse response = target.wsConnector().call(request);
+ profiler.stopDebug(format("%s %d %s", request.getMethod(), response.code(), response.requestUrl()));
+ failIfUnauthorized(response);
+ return response;
+ }
+
+ public String baseUrl() {
+ return target.wsConnector().baseUrl();
+ }
+
+ @VisibleForTesting
+ WsConnector wsConnector() {
+ return target.wsConnector();
+ }
+
+ private void failIfUnauthorized(WsResponse response) {
+ if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ if (hasCredentials) {
+ // credentials are not valid
+ throw MessageException.of(format("Not authorized. Please check the properties %s and %s.",
+ CoreProperties.LOGIN, CoreProperties.PASSWORD));
+ }
+ // not authenticated - see https://jira.sonarsource.com/browse/SONAR-4048
+ throw MessageException.of(format("Not authorized. Analyzing this project requires to be authenticated. " +
+ "Please provide the values of the properties %s and %s.", CoreProperties.LOGIN, CoreProperties.PASSWORD));
+
+ }
+ if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) {
+ // SONAR-4397 Details are in response content
+ throw MessageException.of(tryParseAsJsonError(response.content()));
+ }
+ response.failIfNotSuccessful();
+ }
+
+ private static String tryParseAsJsonError(String responseContent) {
+ try {
+ JsonParser parser = new JsonParser();
+ JsonObject obj = parser.parse(responseContent).getAsJsonObject();
+ JsonArray errors = obj.getAsJsonArray("errors");
+ List<String> errorMessages = new ArrayList<>();
+ for (JsonElement e : errors) {
+ errorMessages.add(e.getAsJsonObject().get("msg").getAsString());
+ }
+ return Joiner.on(", ").join(errorMessages);
+ } catch (Exception e) {
+ return responseContent;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClientProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClientProvider.java
new file mode 100644
index 00000000000..d33baf821e5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClientProvider.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.batch.bootstrapper.EnvironmentInformation;
+import org.sonarqube.ws.client.HttpConnector;
+import org.sonarqube.ws.client.HttpWsClient;
+
+import static java.lang.Integer.parseInt;
+import static java.lang.String.valueOf;
+import static org.apache.commons.lang.StringUtils.defaultIfBlank;
+
+@BatchSide
+public class BatchWsClientProvider extends ProviderAdapter {
+
+ static final int CONNECT_TIMEOUT_MS = 5_000;
+ static final String READ_TIMEOUT_SEC_PROPERTY = "sonar.ws.timeout";
+ static final int DEFAULT_READ_TIMEOUT_SEC = 60;
+
+ private BatchWsClient wsClient;
+
+ public synchronized BatchWsClient provide(final GlobalProperties settings, final EnvironmentInformation env) {
+ if (wsClient == null) {
+ String url = defaultIfBlank(settings.property("sonar.host.url"), CoreProperties.SERVER_BASE_URL_DEFAULT_VALUE);
+ HttpConnector.Builder builder = new HttpConnector.Builder();
+
+ // TODO proxy
+
+ String timeoutSec = defaultIfBlank(settings.property(READ_TIMEOUT_SEC_PROPERTY), valueOf(DEFAULT_READ_TIMEOUT_SEC));
+ String login = defaultIfBlank(settings.property(CoreProperties.LOGIN), null);
+ builder
+ .readTimeoutMilliseconds(parseInt(timeoutSec) * 1_000)
+ .connectTimeoutMilliseconds(CONNECT_TIMEOUT_MS)
+ .userAgent(env.toString())
+ .url(url)
+ .credentials(login, settings.property(CoreProperties.PASSWORD));
+
+ wsClient = new BatchWsClient(new HttpWsClient(builder.build()), login != null);
+ }
+ return wsClient;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/DroppedPropertyChecker.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/DroppedPropertyChecker.java
new file mode 100644
index 00000000000..72b34d613ea
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/DroppedPropertyChecker.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.Map;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.util.Objects.requireNonNull;
+
+public class DroppedPropertyChecker {
+
+ private static final Logger LOG = Loggers.get(DroppedPropertyChecker.class);
+
+ private final Map<String, String> settings;
+ private final Map<String, String> properties;
+
+ public DroppedPropertyChecker(Map<String, String> properties, Map<String, String> droppedPropertiesAndMsg) {
+ this.settings = requireNonNull(properties);
+ this.properties = requireNonNull(droppedPropertiesAndMsg);
+ }
+
+ public void checkDroppedProperties() {
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ if (settings.containsKey(entry.getKey())) {
+ LOG.warn("Property '{}' is not supported any more. {}", entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionInstaller.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionInstaller.java
new file mode 100644
index 00000000000..ff610139032
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionInstaller.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.api.ExtensionProvider;
+import org.sonar.api.SonarPlugin;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.core.platform.ComponentContainer;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginRepository;
+
+public class ExtensionInstaller {
+
+ private final PluginRepository pluginRepository;
+ private final AnalysisMode analysisMode;
+
+ public ExtensionInstaller(PluginRepository pluginRepository, AnalysisMode analysisMode) {
+ this.pluginRepository = pluginRepository;
+ this.analysisMode = analysisMode;
+ }
+
+ public ExtensionInstaller install(ComponentContainer container, ExtensionMatcher matcher) {
+
+ // core components
+ for (Object o : BatchComponents.all(analysisMode)) {
+ doInstall(container, matcher, null, o);
+ }
+
+ // plugin extensions
+ for (PluginInfo pluginInfo : pluginRepository.getPluginInfos()) {
+ SonarPlugin plugin = pluginRepository.getPluginInstance(pluginInfo.getKey());
+ for (Object extension : plugin.getExtensions()) {
+ doInstall(container, matcher, pluginInfo, extension);
+ }
+ }
+ List<ExtensionProvider> providers = container.getComponentsByType(ExtensionProvider.class);
+ for (ExtensionProvider provider : providers) {
+ Object object = provider.provide();
+ if (object instanceof Iterable) {
+ for (Object extension : (Iterable) object) {
+ doInstall(container, matcher, null, extension);
+ }
+ } else {
+ doInstall(container, matcher, null, object);
+ }
+ }
+ return this;
+ }
+
+ private static void doInstall(ComponentContainer container, ExtensionMatcher matcher, @Nullable PluginInfo pluginInfo, Object extension) {
+ if (matcher.accept(extension)) {
+ container.addExtension(pluginInfo, extension);
+ } else {
+ container.declareExtension(pluginInfo, extension);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionMatcher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionMatcher.java
new file mode 100644
index 00000000000..d38581f572d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionMatcher.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.sonar.api.batch.BatchSide;
+
+/**
+ * @since 3.6
+ */
+@BatchSide
+public interface ExtensionMatcher {
+ boolean accept(Object extension);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionUtils.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionUtils.java
new file mode 100644
index 00000000000..adf16a40315
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/ExtensionUtils.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.utils.AnnotationUtils;
+
+public class ExtensionUtils {
+
+ private ExtensionUtils() {
+ // only static methods
+ }
+
+ public static boolean isInstantiationStrategy(Object extension, String strategy) {
+ InstantiationStrategy annotation = AnnotationUtils.getAnnotation(extension, InstantiationStrategy.class);
+ if (annotation != null) {
+ return strategy.equals(annotation.value());
+ }
+ return InstantiationStrategy.PER_PROJECT.equals(strategy);
+ }
+
+ public static boolean isBatchSide(Object extension) {
+ return AnnotationUtils.getAnnotation(extension, BatchSide.class) != null;
+ }
+
+ public static boolean isType(Object extension, Class<?> extensionClass) {
+ Class clazz = extension instanceof Class ? (Class) extension : extension.getClass();
+ return extensionClass.isAssignableFrom(clazz);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/FileCacheProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/FileCacheProvider.java
new file mode 100644
index 00000000000..9880c7de23b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/FileCacheProvider.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.config.Settings;
+import org.sonar.home.cache.FileCache;
+import org.sonar.home.cache.FileCacheBuilder;
+
+public class FileCacheProvider extends ProviderAdapter {
+ private FileCache cache;
+
+ public FileCache provide(Settings settings) {
+ if (cache == null) {
+ String home = settings.getString("sonar.userHome");
+ cache = new FileCacheBuilder(new Slf4jLogger()).setUserHome(home).build();
+ }
+ return cache;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java
new file mode 100644
index 00000000000..8ac69fd6258
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java
@@ -0,0 +1,129 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.List;
+import java.util.Map;
+import org.sonar.api.SonarPlugin;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.UriReader;
+import org.sonar.batch.cache.GlobalPersistentCacheProvider;
+import org.sonar.batch.cache.ProjectSyncContainer;
+import org.sonar.batch.cache.StrategyWSLoaderProvider;
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.batch.index.CachesManager;
+import org.sonar.batch.platform.DefaultServer;
+import org.sonar.batch.repository.DefaultGlobalRepositoriesLoader;
+import org.sonar.batch.repository.GlobalRepositoriesLoader;
+import org.sonar.batch.repository.GlobalRepositoriesProvider;
+import org.sonar.batch.task.TaskContainer;
+import org.sonar.core.platform.ComponentContainer;
+import org.sonar.core.platform.PluginClassloaderFactory;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginLoader;
+import org.sonar.core.platform.PluginRepository;
+import org.sonar.core.util.DefaultHttpDownloader;
+import org.sonar.core.util.UuidFactoryImpl;
+
+public class GlobalContainer extends ComponentContainer {
+
+ private final Map<String, String> bootstrapProperties;
+ private boolean preferCache;
+
+ private GlobalContainer(Map<String, String> bootstrapProperties, boolean preferCache) {
+ super();
+ this.bootstrapProperties = bootstrapProperties;
+ this.preferCache = preferCache;
+ }
+
+ public static GlobalContainer create(Map<String, String> bootstrapProperties, List<?> extensions, boolean preferCache) {
+ GlobalContainer container = new GlobalContainer(bootstrapProperties, preferCache);
+ container.add(extensions);
+ return container;
+ }
+
+ @Override
+ protected void doBeforeStart() {
+ GlobalProperties bootstrapProps = new GlobalProperties(bootstrapProperties);
+ GlobalMode globalMode = new GlobalMode(bootstrapProps);
+ LoadStrategy strategy = getDataLoadingStrategy(globalMode, preferCache);
+ StrategyWSLoaderProvider wsLoaderProvider = new StrategyWSLoaderProvider(strategy);
+ add(wsLoaderProvider);
+ add(bootstrapProps);
+ add(globalMode);
+ addBootstrapComponents();
+ }
+
+ private static LoadStrategy getDataLoadingStrategy(GlobalMode mode, boolean preferCache) {
+ if (!mode.isIssues()) {
+ return LoadStrategy.SERVER_ONLY;
+ }
+
+ return preferCache ? LoadStrategy.CACHE_FIRST : LoadStrategy.SERVER_FIRST;
+ }
+
+ private void addBootstrapComponents() {
+ add(
+ // plugins
+ BatchPluginRepository.class,
+ PluginLoader.class,
+ PluginClassloaderFactory.class,
+ BatchPluginJarExploder.class,
+ BatchPluginPredicate.class,
+ ExtensionInstaller.class,
+
+ CachesManager.class,
+ GlobalSettings.class,
+ new BatchWsClientProvider(),
+ DefaultServer.class,
+ new GlobalTempFolderProvider(),
+ DefaultHttpDownloader.class,
+ UriReader.class,
+ new FileCacheProvider(),
+ new GlobalPersistentCacheProvider(),
+ System2.INSTANCE,
+ new GlobalRepositoriesProvider(),
+ UuidFactoryImpl.INSTANCE);
+ addIfMissing(BatchPluginInstaller.class, PluginInstaller.class);
+ addIfMissing(DefaultGlobalRepositoriesLoader.class, GlobalRepositoriesLoader.class);
+ }
+
+ @Override
+ protected void doAfterStart() {
+ installPlugins();
+ }
+
+ private void installPlugins() {
+ PluginRepository pluginRepository = getComponentByType(PluginRepository.class);
+ for (PluginInfo pluginInfo : pluginRepository.getPluginInfos()) {
+ SonarPlugin instance = pluginRepository.getPluginInstance(pluginInfo.getKey());
+ addExtension(pluginInfo, instance);
+ }
+ }
+
+ public void executeTask(Map<String, String> taskProperties, Object... components) {
+ new TaskContainer(this, taskProperties, components).execute();
+ }
+
+ public void syncProject(String projectKey, boolean force) {
+ new ProjectSyncContainer(this, projectKey, force).execute();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalMode.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalMode.java
new file mode 100644
index 00000000000..774dac71361
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalMode.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.sonar.api.CoreProperties;
+
+import org.sonar.api.batch.AnalysisMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GlobalMode extends AbstractAnalysisMode implements AnalysisMode {
+ private static final Logger LOG = LoggerFactory.getLogger(GlobalMode.class);
+
+ public GlobalMode(GlobalProperties props) {
+ String mode = props.property(CoreProperties.ANALYSIS_MODE);
+ validate(mode);
+ issues = CoreProperties.ANALYSIS_MODE_ISSUES.equals(mode) || CoreProperties.ANALYSIS_MODE_PREVIEW.equals(mode);
+
+ if (preview) {
+ LOG.debug("Preview global mode");
+ } else if (issues) {
+ LOG.debug("Issues global mode");
+ } else {
+ LOG.debug("Publish global mode");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalProperties.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalProperties.java
new file mode 100644
index 00000000000..62bc070080e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalProperties.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.sonar.api.CoreProperties;
+
+import java.util.Map;
+
+/**
+ * Immutable batch properties that are not specific to a task (for example
+ * coming from global configuration file of sonar-runner).
+ */
+public class GlobalProperties extends UserProperties {
+
+ public GlobalProperties(Map<String, String> properties) {
+ super(properties, properties.get(CoreProperties.ENCRYPTION_SECRET_KEY_PATH));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalSettings.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalSettings.java
new file mode 100644
index 00000000000..3ff5dce8ecd
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalSettings.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+
+public class GlobalSettings extends Settings {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GlobalSettings.class);
+
+ private static final String JDBC_SPECIFIC_MESSAGE = "It will be ignored. There is no longer any DB connection to the SQ database.";
+ /**
+ * A map of dropped properties as key and specific message to display for that property
+ * (what will happen, what should the user do, ...) as a value
+ */
+ private static final Map<String, String> DROPPED_PROPERTIES = ImmutableMap.of(
+ "sonar.jdbc.url", JDBC_SPECIFIC_MESSAGE,
+ "sonar.jdbc.username", JDBC_SPECIFIC_MESSAGE,
+ "sonar.jdbc.password", JDBC_SPECIFIC_MESSAGE);
+
+ private final GlobalProperties bootstrapProps;
+ private final GlobalRepositories globalReferentials;
+ private final GlobalMode mode;
+
+ public GlobalSettings(GlobalProperties bootstrapProps, PropertyDefinitions propertyDefinitions,
+ GlobalRepositories globalReferentials, GlobalMode mode) {
+
+ super(propertyDefinitions);
+ this.mode = mode;
+ getEncryption().setPathToSecretKey(bootstrapProps.property(CoreProperties.ENCRYPTION_SECRET_KEY_PATH));
+ this.bootstrapProps = bootstrapProps;
+ this.globalReferentials = globalReferentials;
+ init();
+ new DroppedPropertyChecker(this.getProperties(), DROPPED_PROPERTIES).checkDroppedProperties();
+ }
+
+ private void init() {
+ addProperties(globalReferentials.globalSettings());
+ addProperties(bootstrapProps.properties());
+
+ if (hasKey(CoreProperties.PERMANENT_SERVER_ID)) {
+ LOG.info("Server id: " + getString(CoreProperties.PERMANENT_SERVER_ID));
+ }
+ }
+
+ @Override
+ protected void doOnGetProperties(String key) {
+ if (mode.isIssues() && key.endsWith(".secured") && !key.contains(".license")) {
+ throw MessageException.of("Access to the secured property '" + key
+ + "' is not possible in issues mode. The SonarQube plugin which requires this property must be deactivated in issues mode.");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalTempFolderProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalTempFolderProvider.java
new file mode 100644
index 00000000000..2ea3bff55a3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/GlobalTempFolderProvider.java
@@ -0,0 +1,175 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang.StringUtils;
+import org.picocontainer.ComponentLifecycle;
+import org.picocontainer.PicoContainer;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.TempFolder;
+import org.sonar.api.utils.internal.DefaultTempFolder;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static org.sonar.core.util.FileUtils.deleteQuietly;
+
+public class GlobalTempFolderProvider extends ProviderAdapter implements ComponentLifecycle<TempFolder> {
+ private static final Logger LOG = Loggers.get(GlobalTempFolderProvider.class);
+ private static final long CLEAN_MAX_AGE = TimeUnit.DAYS.toMillis(21);
+ static final String TMP_NAME_PREFIX = ".sonartmp_";
+ private boolean started = false;
+
+ private System2 system;
+ private DefaultTempFolder tempFolder;
+
+ public GlobalTempFolderProvider() {
+ this(new System2());
+ }
+
+ GlobalTempFolderProvider(System2 system) {
+ this.system = system;
+ }
+
+ public TempFolder provide(GlobalProperties bootstrapProps) {
+ if (tempFolder == null) {
+
+ String workingPathName = StringUtils.defaultIfBlank(bootstrapProps.property(CoreProperties.GLOBAL_WORKING_DIRECTORY), CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE);
+ Path workingPath = Paths.get(workingPathName);
+
+ if (!workingPath.isAbsolute()) {
+ Path home = findSonarHome(bootstrapProps);
+ workingPath = home.resolve(workingPath).normalize();
+ }
+
+ try {
+ cleanTempFolders(workingPath);
+ } catch (IOException e) {
+ LOG.error(String.format("failed to clean global working directory: %s", workingPath), e);
+ }
+ Path tempDir = createTempFolder(workingPath);
+ tempFolder = new DefaultTempFolder(tempDir.toFile(), true);
+ }
+ return tempFolder;
+ }
+
+ private static Path createTempFolder(Path workingPath) {
+ try {
+ Files.createDirectories(workingPath);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create working path: " + workingPath, e);
+ }
+
+ try {
+ return Files.createTempDirectory(workingPath, TMP_NAME_PREFIX);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create temporary folder in " + workingPath, e);
+ }
+ }
+
+ private Path findSonarHome(GlobalProperties props) {
+ String home = props.property("sonar.userHome");
+ if (home != null) {
+ return Paths.get(home).toAbsolutePath();
+ }
+
+ home = system.envVariable("SONAR_USER_HOME");
+
+ if (home != null) {
+ return Paths.get(home).toAbsolutePath();
+ }
+
+ home = system.property("user.home");
+ return Paths.get(home, ".sonar").toAbsolutePath();
+ }
+
+ private static void cleanTempFolders(Path path) throws IOException {
+ if (Files.exists(path)) {
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, new CleanFilter())) {
+ for (Path p : stream) {
+ deleteQuietly(p.toFile());
+ }
+ }
+ }
+ }
+
+ private static class CleanFilter implements DirectoryStream.Filter<Path> {
+ @Override
+ public boolean accept(Path path) throws IOException {
+ if (!Files.isDirectory(path)) {
+ return false;
+ }
+
+ if (!path.getFileName().toString().startsWith(TMP_NAME_PREFIX)) {
+ return false;
+ }
+
+ long threshold = System.currentTimeMillis() - CLEAN_MAX_AGE;
+
+ // we could also check the timestamp in the name, instead
+ BasicFileAttributes attrs;
+
+ try {
+ attrs = Files.readAttributes(path, BasicFileAttributes.class);
+ } catch (IOException ioe) {
+ LOG.error(String.format("Couldn't read file attributes for %s : ", path), ioe);
+ return false;
+ }
+
+ long creationTime = attrs.creationTime().toMillis();
+ return creationTime < threshold;
+ }
+ }
+
+ @Override
+ public void start(PicoContainer container) {
+ started = true;
+ }
+
+ @Override
+ public void stop(PicoContainer container) {
+ if (tempFolder != null) {
+ tempFolder.stop();
+ }
+ }
+
+ @Override
+ public void dispose(PicoContainer container) {
+ //nothing to do
+ }
+
+ @Override
+ public boolean componentHasLifecycle() {
+ return true;
+ }
+
+ @Override
+ public boolean isStarted() {
+ return started;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/MetricProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/MetricProvider.java
new file mode 100644
index 00000000000..1735ab4a79b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/MetricProvider.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.ExtensionProvider;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.Metrics;
+
+import java.util.List;
+
+@BatchSide
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+public class MetricProvider extends ExtensionProvider {
+
+ private Metrics[] factories;
+
+ public MetricProvider(Metrics[] factories) {
+ this.factories = factories;
+ }
+
+ public MetricProvider() {
+ this.factories = new Metrics[0];
+ }
+
+ @Override
+ public List<Metric> provide() {
+ List<Metric> metrics = Lists.newArrayList(CoreMetrics.getMetrics());
+ for (Metrics factory : factories) {
+ metrics.addAll(factory.getMetrics());
+ }
+ return metrics;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/PluginInstaller.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/PluginInstaller.java
new file mode 100644
index 00000000000..64f923bd9a5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/PluginInstaller.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.Map;
+import org.sonar.api.SonarPlugin;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.core.platform.PluginInfo;
+
+@BatchSide
+public interface PluginInstaller {
+
+ /**
+ * Gets the list of plugins installed on server and downloads them if not
+ * already in local cache.
+ * @return information about all installed plugins, grouped by key
+ */
+ Map<String, PluginInfo> installRemotes();
+
+ /**
+ * Used only by tests.
+ * @see org.sonar.batch.mediumtest.BatchMediumTester
+ */
+ Map<String, SonarPlugin> installLocals();
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/Slf4jLogger.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/Slf4jLogger.java
new file mode 100644
index 00000000000..270bc10fe0d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/Slf4jLogger.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Slf4jLogger implements org.sonar.home.cache.Logger {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Slf4jLogger.class);
+
+ @Override
+ public void debug(String msg) {
+ LOG.debug(msg);
+ }
+
+ @Override
+ public void info(String msg) {
+ LOG.info(msg);
+ }
+
+ @Override
+ public void warn(String msg) {
+ LOG.warn(msg);
+ }
+
+ @Override
+ public void error(String msg, Throwable t) {
+ LOG.error(msg, t);
+ }
+
+ @Override
+ public void error(String msg) {
+ LOG.error(msg);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/UserProperties.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/UserProperties.java
new file mode 100644
index 00000000000..d16f8e72d27
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/UserProperties.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.Maps;
+import org.sonar.api.config.Encryption;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+/**
+ * Properties that are coming from bootstrapper.
+ */
+public abstract class UserProperties {
+
+ private final Map<String, String> properties;
+ private final Encryption encryption;
+
+ public UserProperties(Map<String, String> properties, @Nullable String pathToSecretKey) {
+ encryption = new Encryption(pathToSecretKey);
+ Map<String, String> decryptedProps = Maps.newHashMap();
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ String value = entry.getValue();
+ if (value != null && encryption.isEncrypted(value)) {
+ try {
+ value = encryption.decrypt(value);
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to decrypt the property " + entry.getKey() + ". Please check your secret key.", e);
+ }
+ }
+ decryptedProps.put(entry.getKey(), value);
+ }
+ this.properties = Maps.newHashMap(decryptedProps);
+ }
+
+ public Map<String, String> properties() {
+ return properties;
+ }
+
+ public String property(String key) {
+ return properties.get(key);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/package-info.java
new file mode 100644
index 00000000000..0efbab080ec
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.bootstrap;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java
new file mode 100644
index 00000000000..d5d3da13087
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java
@@ -0,0 +1,274 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import org.sonar.api.utils.MessageException;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.picocontainer.annotations.Nullable;
+import org.sonar.batch.bootstrap.GlobalContainer;
+
+/**
+ * Entry point for sonar-runner 2.1.
+ *
+ * @since 2.14
+ */
+public final class Batch {
+
+ private boolean started = false;
+ private LoggingConfiguration loggingConfig;
+ private List<Object> components;
+ private Map<String, String> bootstrapProperties = Maps.newHashMap();
+ private GlobalContainer bootstrapContainer;
+
+ private Batch(Builder builder) {
+ components = Lists.newArrayList();
+ components.addAll(builder.components);
+ if (builder.environment != null) {
+ components.add(builder.environment);
+ }
+ if (builder.bootstrapProperties != null) {
+ bootstrapProperties.putAll(builder.bootstrapProperties);
+ }
+ if (builder.isEnableLoggingConfiguration()) {
+ loggingConfig = new LoggingConfiguration(builder.environment).setProperties(bootstrapProperties);
+
+ if (builder.logOutput != null) {
+ loggingConfig.setLogOutput(builder.logOutput);
+ }
+ }
+ }
+
+ public LoggingConfiguration getLoggingConfiguration() {
+ return loggingConfig;
+ }
+
+ /**
+ * @deprecated since 4.4 use {@link #start()}, {@link #executeTask(Map)} and then {@link #stop()}
+ */
+ @Deprecated
+ public synchronized Batch execute() {
+ configureLogging();
+ start();
+ boolean threw = true;
+ try {
+ executeTask(bootstrapProperties);
+ threw = false;
+ } finally {
+ doStop(threw);
+ }
+
+ return this;
+ }
+
+ /**
+ * @since 4.4
+ */
+ public synchronized Batch start() {
+ return start(false);
+ }
+
+ public synchronized Batch start(boolean preferCache) {
+ if (started) {
+ throw new IllegalStateException("Batch is already started");
+ }
+
+ configureLogging();
+ try {
+ bootstrapContainer = GlobalContainer.create(bootstrapProperties, components, preferCache);
+ bootstrapContainer.startComponents();
+ } catch (RuntimeException e) {
+ throw handleException(e);
+ }
+ this.started = true;
+
+ return this;
+ }
+
+ /**
+ * @since 4.4
+ */
+ public Batch executeTask(Map<String, String> analysisProperties, Object... components) {
+ checkStarted();
+ configureTaskLogging(analysisProperties);
+ try {
+ bootstrapContainer.executeTask(analysisProperties, components);
+ } catch (RuntimeException e) {
+ throw handleException(e);
+ }
+ return this;
+ }
+
+ /**
+ * @since 5.2
+ */
+ public Batch executeTask(Map<String, String> analysisProperties, IssueListener issueListener) {
+ checkStarted();
+ configureTaskLogging(analysisProperties);
+ try {
+ bootstrapContainer.executeTask(analysisProperties, components, issueListener);
+ } catch (RuntimeException e) {
+ throw handleException(e);
+ }
+ return this;
+ }
+
+ private void checkStarted() {
+ if (!started) {
+ throw new IllegalStateException("Batch is not started. Unable to execute task.");
+ }
+ }
+
+ private RuntimeException handleException(RuntimeException t) {
+ if (loggingConfig.isVerbose()) {
+ return t;
+ }
+
+ for (Throwable y : Throwables.getCausalChain(t)) {
+ if (y instanceof MessageException) {
+ return (MessageException) y;
+ }
+ }
+
+ return t;
+ }
+
+ /**
+ * @since 5.2
+ */
+ public Batch syncProject(String projectKey) {
+ checkStarted();
+ bootstrapContainer.syncProject(projectKey, true);
+ return this;
+ }
+
+ /**
+ * @since 4.4
+ */
+ public synchronized void stop() {
+ doStop(false);
+ }
+
+ private void doStop(boolean swallowException) {
+ checkStarted();
+ configureLogging();
+ try {
+ bootstrapContainer.stopComponents(swallowException);
+ } catch (RuntimeException e) {
+ throw handleException(e);
+ }
+ this.started = false;
+ }
+
+ private void configureLogging() {
+ if (loggingConfig != null) {
+ loggingConfig.setProperties(bootstrapProperties);
+ LoggingConfigurator.apply(loggingConfig);
+ }
+ }
+
+ private void configureTaskLogging(Map<String, String> taskProperties) {
+ if (loggingConfig != null) {
+ loggingConfig.setProperties(taskProperties, bootstrapProperties);
+ LoggingConfigurator.apply(loggingConfig);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private Map<String, String> bootstrapProperties;
+ private EnvironmentInformation environment;
+ private List<Object> components = Lists.newArrayList();
+ private boolean enableLoggingConfiguration = true;
+ private LogOutput logOutput;
+
+ private Builder() {
+ }
+
+ public Builder setEnvironment(EnvironmentInformation env) {
+ this.environment = env;
+ return this;
+ }
+
+ public Builder setComponents(List<Object> l) {
+ this.components = l;
+ return this;
+ }
+
+ public Builder setLogOutput(@Nullable LogOutput logOutput) {
+ this.logOutput = logOutput;
+ return this;
+ }
+
+ /**
+ * @deprecated since 3.7 use {@link #setBootstrapProperties(Map)}
+ */
+ @Deprecated
+ public Builder setGlobalProperties(Map<String, String> globalProperties) {
+ this.bootstrapProperties = globalProperties;
+ return this;
+ }
+
+ public Builder setBootstrapProperties(Map<String, String> bootstrapProperties) {
+ this.bootstrapProperties = bootstrapProperties;
+ return this;
+ }
+
+ public Builder addComponents(Object... components) {
+ Collections.addAll(this.components, components);
+ return this;
+ }
+
+ public Builder addComponent(Object component) {
+ this.components.add(component);
+ return this;
+ }
+
+ public boolean isEnableLoggingConfiguration() {
+ return enableLoggingConfiguration;
+ }
+
+ /**
+ * Logback is configured by default. It can be disabled, but n this case the batch bootstrapper must provide its
+ * own implementation of SLF4J.
+ */
+ public Builder setEnableLoggingConfiguration(boolean b) {
+ this.enableLoggingConfiguration = b;
+ return this;
+ }
+
+ public Batch build() {
+ if (components == null) {
+ throw new IllegalStateException("Batch components are not set");
+ }
+ return new Batch(this);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/EnvironmentInformation.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/EnvironmentInformation.java
new file mode 100644
index 00000000000..a04da9df40a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/EnvironmentInformation.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import org.sonar.api.batch.BatchSide;
+
+/**
+ * Describes execution environment.
+ *
+ * @since 2.6
+ */
+@BatchSide
+public class EnvironmentInformation {
+
+ private String key;
+ private String version;
+
+ public EnvironmentInformation(String key, String version) {
+ this.key = key;
+ this.version = version;
+ }
+
+ /**
+ * @return unique key of environment, for example - "maven", "ant"
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * @return version of environment, for example Maven can have "2.2.1" or "3.0.2",
+ * but there is no guarantees about format - it's just a string.
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%s", key, version);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/IssueListener.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/IssueListener.java
new file mode 100644
index 00000000000..bf3f399142a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/IssueListener.java
@@ -0,0 +1,167 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+public interface IssueListener {
+ void handle(Issue issue);
+
+ class Issue {
+ /** @since 5.3 */
+ private Integer startLine;
+ /** @since 5.3 */
+ private Integer startLineOffset;
+ /** @since 5.3 */
+ private Integer endLine;
+ /** @since 5.3 */
+ private Integer endLineOffset;
+
+ private String key;
+ private String componentKey;
+ private String message;
+ private String ruleKey;
+ private String ruleName;
+ private String status;
+ private String resolution;
+ private boolean isNew;
+ private String assigneeLogin;
+ private String assigneeName;
+ private String severity;
+
+ public String getSeverity() {
+ return severity;
+ }
+
+ public void setSeverity(String severity) {
+ this.severity = severity;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getComponentKey() {
+ return componentKey;
+ }
+
+ public void setComponentKey(String componentKey) {
+ this.componentKey = componentKey;
+ }
+
+ public Integer getStartLine() {
+ return startLine;
+ }
+
+ public void setStartLine(Integer startLine) {
+ this.startLine = startLine;
+ }
+
+ public Integer getStartLineOffset() {
+ return startLineOffset;
+ }
+
+ public void setStartLineOffset(Integer startLineOffset) {
+ this.startLineOffset = startLineOffset;
+ }
+
+ public Integer getEndLine() {
+ return endLine;
+ }
+
+ public void setEndLine(Integer endLine) {
+ this.endLine = endLine;
+ }
+
+ public Integer getEndLineOffset() {
+ return endLineOffset;
+ }
+
+ public void setEndLineOffset(Integer endLineOffset) {
+ this.endLineOffset = endLineOffset;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getRuleKey() {
+ return ruleKey;
+ }
+
+ public void setRuleKey(String ruleKey) {
+ this.ruleKey = ruleKey;
+ }
+
+ public String getRuleName() {
+ return ruleName;
+ }
+
+ public void setRuleName(String ruleName) {
+ this.ruleName = ruleName;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getResolution() {
+ return resolution;
+ }
+
+ public void setResolution(String resolution) {
+ this.resolution = resolution;
+ }
+
+ public boolean isNew() {
+ return isNew;
+ }
+
+ public void setNew(boolean isNew) {
+ this.isNew = isNew;
+ }
+
+ public String getAssigneeLogin() {
+ return assigneeLogin;
+ }
+
+ public void setAssigneeLogin(String assigneeLogin) {
+ this.assigneeLogin = assigneeLogin;
+ }
+
+ public String getAssigneeName() {
+ return assigneeName;
+ }
+
+ public void setAssigneeName(String assigneeName) {
+ this.assigneeName = assigneeName;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogCallbackAppender.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogCallbackAppender.java
new file mode 100644
index 00000000000..183cd378711
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogCallbackAppender.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.UnsynchronizedAppenderBase;
+
+public class LogCallbackAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
+ protected LogOutput target;
+
+ public LogCallbackAppender(LogOutput target) {
+ setTarget(target);
+ }
+
+ public void setTarget(LogOutput target) {
+ this.target = target;
+ }
+
+ @Override
+ protected void append(ILoggingEvent event) {
+ target.log(event.getFormattedMessage(), translate(event.getLevel()));
+ }
+
+ private static LogOutput.Level translate(Level level) {
+ switch (level.toInt()) {
+ case Level.ERROR_INT:
+ return LogOutput.Level.ERROR;
+ case Level.WARN_INT:
+ return LogOutput.Level.WARN;
+ case Level.INFO_INT:
+ return LogOutput.Level.INFO;
+ case Level.TRACE_INT:
+ return LogOutput.Level.TRACE;
+ case Level.DEBUG_INT:
+ default:
+ return LogOutput.Level.DEBUG;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogOutput.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogOutput.java
new file mode 100644
index 00000000000..b69c405d931
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LogOutput.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+/**
+ * Allow to redirect batch logs to a custom output. By defaults logs are written to System.out
+ * @since 5.2
+ */
+public interface LogOutput {
+
+ void log(String formattedMessage, Level level);
+
+ enum Level {
+ ERROR, WARN, INFO, DEBUG, TRACE;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfiguration.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfiguration.java
new file mode 100644
index 00000000000..41a741bbd08
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfiguration.java
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @since 2.14
+ */
+public final class LoggingConfiguration {
+
+ public static final String PROPERTY_ROOT_LOGGER_LEVEL = "ROOT_LOGGER_LEVEL";
+ public static final String PROPERTY_SQL_LOGGER_LEVEL = "SQL_LOGGER_LEVEL";
+
+ public static final String PROPERTY_FORMAT = "FORMAT";
+
+ public static final String LEVEL_ROOT_VERBOSE = "DEBUG";
+ public static final String LEVEL_ROOT_DEFAULT = "INFO";
+
+ @VisibleForTesting
+ static final String FORMAT_DEFAULT = "%d{HH:mm:ss.SSS} %-5level - %msg%n";
+ @VisibleForTesting
+ static final String FORMAT_MAVEN = "[%level] [%d{HH:mm:ss.SSS}] %msg%n";
+
+ private Map<String, String> substitutionVariables = Maps.newHashMap();
+ private LogOutput logOutput = null;
+ private boolean verbose;
+
+ public LoggingConfiguration() {
+ this(null);
+ }
+
+ public LoggingConfiguration(@Nullable EnvironmentInformation environment) {
+ setVerbose(false);
+ if (environment != null && "maven".equalsIgnoreCase(environment.getKey())) {
+ setFormat(FORMAT_MAVEN);
+ } else {
+ setFormat(FORMAT_DEFAULT);
+ }
+ }
+
+ public LoggingConfiguration setProperties(Map<String, String> properties) {
+ setShowSql(properties, null);
+ setVerbose(properties, null);
+ return this;
+ }
+
+ public LoggingConfiguration setProperties(Map<String, String> properties, @Nullable Map<String, String> fallback) {
+ setShowSql(properties, fallback);
+ setVerbose(properties, fallback);
+ return this;
+ }
+
+ public LoggingConfiguration setLogOutput(@Nullable LogOutput listener) {
+ this.logOutput = listener;
+ return this;
+ }
+
+ public LoggingConfiguration setVerbose(boolean verbose) {
+ return setRootLevel(verbose ? LEVEL_ROOT_VERBOSE : LEVEL_ROOT_DEFAULT);
+ }
+
+ public boolean isVerbose() {
+ return verbose;
+ }
+
+ public LoggingConfiguration setVerbose(Map<String, String> props, @Nullable Map<String, String> fallback) {
+ String logLevel = getFallback("sonar.log.level", props, fallback);
+ String deprecatedProfilingLevel = getFallback("sonar.log.profilingLevel", props, fallback);
+ verbose = "true".equals(getFallback("sonar.verbose", props, fallback)) ||
+ "DEBUG".equals(logLevel) || "TRACE".equals(logLevel) ||
+ "BASIC".equals(deprecatedProfilingLevel) || "FULL".equals(deprecatedProfilingLevel);
+
+ return setVerbose(verbose);
+ }
+
+ @CheckForNull
+ private static String getFallback(String key, Map<String, String> properties, @Nullable Map<String, String> fallback) {
+ if (properties.containsKey(key)) {
+ return properties.get(key);
+ }
+
+ if (fallback != null) {
+ return fallback.get(key);
+ }
+
+ return null;
+ }
+
+ public LoggingConfiguration setRootLevel(String level) {
+ return addSubstitutionVariable(PROPERTY_ROOT_LOGGER_LEVEL, level);
+ }
+
+ public LoggingConfiguration setShowSql(boolean showSql) {
+ return addSubstitutionVariable(PROPERTY_SQL_LOGGER_LEVEL, showSql ? "TRACE" : "WARN");
+ }
+
+ public LoggingConfiguration setShowSql(Map<String, String> properties, @Nullable Map<String, String> fallback) {
+ String logLevel = getFallback("sonar.log.level", properties, fallback);
+ String deprecatedProfilingLevel = getFallback("sonar.log.profilingLevel", properties, fallback);
+ boolean sql = "TRACE".equals(logLevel) || "FULL".equals(deprecatedProfilingLevel);
+
+ return setShowSql(sql);
+ }
+
+ @VisibleForTesting
+ LoggingConfiguration setFormat(String format) {
+ return addSubstitutionVariable(PROPERTY_FORMAT, StringUtils.defaultIfBlank(format, FORMAT_DEFAULT));
+ }
+
+ public LoggingConfiguration addSubstitutionVariable(String key, String value) {
+ substitutionVariables.put(key, value);
+ return this;
+ }
+
+ @VisibleForTesting
+ String getSubstitutionVariable(String key) {
+ return substitutionVariables.get(key);
+ }
+
+ Map<String, String> getSubstitutionVariables() {
+ return substitutionVariables;
+ }
+
+ LogOutput getLogOutput() {
+ return logOutput;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfigurator.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfigurator.java
new file mode 100644
index 00000000000..f7f45b1c6fe
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/LoggingConfigurator.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import java.io.File;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.LoggerFactory;
+import org.sonar.core.config.Logback;
+
+public class LoggingConfigurator {
+ private static final String CUSTOM_APPENDER_NAME = "custom_stream";
+
+ private LoggingConfigurator() {
+ }
+
+ public static void apply(LoggingConfiguration conf, File logbackFile) {
+ Logback.configure(logbackFile, conf.getSubstitutionVariables());
+
+ if (conf.getLogOutput() != null) {
+ setCustomRootAppender(conf);
+ }
+ }
+
+ public static void apply(LoggingConfiguration conf) {
+ apply(conf, "/org/sonar/batch/bootstrapper/logback.xml");
+ }
+
+ public static void apply(LoggingConfiguration conf, String classloaderPath) {
+ Logback.configure(classloaderPath, conf.getSubstitutionVariables());
+
+ // if not set, keep default behavior (configured to stdout through the file in classpath)
+ if (conf.getLogOutput() != null) {
+ setCustomRootAppender(conf);
+ }
+ }
+
+ private static void setCustomRootAppender(LoggingConfiguration conf) {
+ Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+ String level = StringUtils.defaultIfBlank(conf.getSubstitutionVariables().get(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL), LoggingConfiguration.LEVEL_ROOT_DEFAULT);
+
+ if (logger.getAppender(CUSTOM_APPENDER_NAME) == null) {
+ logger.detachAndStopAllAppenders();
+ logger.addAppender(createAppender(conf.getLogOutput()));
+ }
+ logger.setLevel(Level.toLevel(level));
+ }
+
+ private static Appender<ILoggingEvent> createAppender(LogOutput target) {
+ LogCallbackAppender appender = new LogCallbackAppender(target);
+ appender.setName(CUSTOM_APPENDER_NAME);
+ appender.start();
+
+ return appender;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/package-info.java
new file mode 100644
index 00000000000..645b8d7dfe2
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/**
+ * This package is a part of bootstrap process, so we should take care about backward compatibility.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.batch.bootstrapper;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java
new file mode 100644
index 00000000000..45baaecacc8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Date;
+import org.sonar.home.cache.PersistentCache;
+
+import static org.sonar.core.util.FileUtils.deleteQuietly;
+
+public class DefaultProjectCacheStatus implements ProjectCacheStatus {
+ private static final String STATUS_FILENAME = "cache-sync-status";
+ private PersistentCache cache;
+
+ public DefaultProjectCacheStatus(PersistentCache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public void save() {
+ Date now = new Date();
+
+ try {
+ try (ObjectOutputStream objOutput = new ObjectOutputStream(new FileOutputStream(getStatusFilePath().toFile()))) {
+ objOutput.writeObject(now);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to write cache sync status", e);
+ }
+ }
+
+ @Override
+ public void delete() {
+ cache.clear();
+ deleteQuietly(getStatusFilePath().toFile());
+ }
+
+ @Override
+ public Date getSyncStatus() {
+ Path p = getStatusFilePath();
+ try {
+ if (!Files.isRegularFile(p)) {
+ return null;
+ }
+ try (ObjectInputStream objInput = new ObjectInputStream(new FileInputStream(p.toFile()))) {
+ return (Date) objInput.readObject();
+ }
+ } catch (IOException | ClassNotFoundException e) {
+ deleteQuietly(p.toFile());
+ throw new IllegalStateException("Failed to read cache sync status", e);
+ }
+ }
+
+ private Path getStatusFilePath() {
+ return cache.getDirectory().resolve(STATUS_FILENAME);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/GlobalPersistentCacheProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/GlobalPersistentCacheProvider.java
new file mode 100644
index 00000000000..6b8677e2ada
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/GlobalPersistentCacheProvider.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import org.apache.commons.lang.StringUtils;
+import org.sonar.batch.bootstrap.Slf4jLogger;
+import org.sonar.home.cache.PersistentCacheBuilder;
+
+import java.nio.file.Paths;
+
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.home.cache.PersistentCache;
+import org.picocontainer.injectors.ProviderAdapter;
+
+public class GlobalPersistentCacheProvider extends ProviderAdapter {
+ private PersistentCache cache;
+
+ public PersistentCache provide(GlobalProperties props) {
+ if (cache == null) {
+ PersistentCacheBuilder builder = new PersistentCacheBuilder(new Slf4jLogger());
+ String home = props.property("sonar.userHome");
+ String serverUrl = getServerUrl(props);
+
+ if (home != null) {
+ builder.setSonarHome(Paths.get(home));
+ }
+
+ builder.setAreaForGlobal(serverUrl);
+ cache = builder.build();
+ }
+
+ return cache;
+ }
+
+ private static String getServerUrl(GlobalProperties props) {
+ return StringUtils.removeEnd(StringUtils.defaultIfBlank(props.property("sonar.host.url"), "http://localhost:9000"), "/");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java
new file mode 100644
index 00000000000..9287c268634
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.batch.repository.QualityProfileLoader;
+import org.sonar.batch.rule.ActiveRulesLoader;
+import org.sonar.batch.rule.RulesLoader;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+public class NonAssociatedCacheSynchronizer {
+ private static final Logger LOG = LoggerFactory.getLogger(NonAssociatedCacheSynchronizer.class);
+
+ private final ProjectCacheStatus cacheStatus;
+ private final QualityProfileLoader qualityProfileLoader;
+ private final ActiveRulesLoader activeRulesLoader;
+ private final RulesLoader rulesLoader;
+
+ public NonAssociatedCacheSynchronizer(RulesLoader rulesLoader, QualityProfileLoader qualityProfileLoader, ActiveRulesLoader activeRulesLoader, ProjectCacheStatus cacheStatus) {
+ this.rulesLoader = rulesLoader;
+ this.qualityProfileLoader = qualityProfileLoader;
+ this.activeRulesLoader = activeRulesLoader;
+ this.cacheStatus = cacheStatus;
+ }
+
+ public void execute(boolean force) {
+ Date lastSync = cacheStatus.getSyncStatus();
+
+ if (lastSync != null) {
+ if (!force) {
+ LOG.info("Found cache [{}]", lastSync);
+ return;
+ } else {
+ LOG.info("-- Found cache [{}], synchronizing data..", lastSync);
+ }
+ } else {
+ LOG.info("-- Cache not found, synchronizing data..");
+ }
+
+ loadData();
+ cacheStatus.save();
+ LOG.info("-- Succesfully synchronized cache");
+ }
+
+ private static Collection<String> getKeys(Collection<QualityProfile> qProfiles) {
+ List<String> list = new ArrayList<>(qProfiles.size());
+ for (QualityProfile qp : qProfiles) {
+ list.add(qp.getKey());
+ }
+
+ return list;
+ }
+
+ private void loadData() {
+ Profiler profiler = Profiler.create(Loggers.get(ProjectCacheSynchronizer.class));
+
+ profiler.startInfo("Load rules");
+ rulesLoader.load(null);
+ profiler.stopInfo();
+
+ profiler.startInfo("Load default quality profiles");
+ Collection<QualityProfile> qProfiles = qualityProfileLoader.loadDefault(null, null);
+ profiler.stopInfo();
+
+ profiler.startInfo("Load default active rules");
+ Collection<String> keys = getKeys(qProfiles);
+ for (String k : keys) {
+ activeRulesLoader.load(k, null);
+ }
+ profiler.stopInfo();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java
new file mode 100644
index 00000000000..ef5fd9754a1
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import java.util.Date;
+
+public interface ProjectCacheStatus {
+ void save();
+
+ void delete();
+
+ Date getSyncStatus();
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java
new file mode 100644
index 00000000000..64010b34c41
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java
@@ -0,0 +1,185 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import com.google.common.base.Function;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.batch.repository.ProjectRepositories;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
+import org.sonar.batch.repository.QualityProfileLoader;
+import org.sonar.batch.repository.ServerIssuesLoader;
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.batch.rule.ActiveRulesLoader;
+import org.sonar.batch.rule.RulesLoader;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+public class ProjectCacheSynchronizer {
+ private static final Logger LOG = LoggerFactory.getLogger(ProjectCacheSynchronizer.class);
+
+ private final ServerIssuesLoader issuesLoader;
+ private final UserRepositoryLoader userRepository;
+ private final ProjectCacheStatus cacheStatus;
+ private final QualityProfileLoader qualityProfileLoader;
+ private final ProjectRepositoriesLoader projectRepositoriesLoader;
+ private final ActiveRulesLoader activeRulesLoader;
+ private final RulesLoader rulesLoader;
+
+ public ProjectCacheSynchronizer(RulesLoader rulesLoader, QualityProfileLoader qualityProfileLoader, ProjectRepositoriesLoader projectSettingsLoader,
+ ActiveRulesLoader activeRulesLoader, ServerIssuesLoader issuesLoader,
+ UserRepositoryLoader userRepository, ProjectCacheStatus cacheStatus) {
+ this.rulesLoader = rulesLoader;
+ this.qualityProfileLoader = qualityProfileLoader;
+ this.projectRepositoriesLoader = projectSettingsLoader;
+ this.activeRulesLoader = activeRulesLoader;
+ this.issuesLoader = issuesLoader;
+ this.userRepository = userRepository;
+ this.cacheStatus = cacheStatus;
+ }
+
+ private static boolean isToday(Date d) {
+ Calendar c1 = Calendar.getInstance();
+ Calendar c2 = Calendar.getInstance();
+ c2.setTime(d);
+
+ return c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR) &&
+ c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR);
+ }
+
+ private static boolean shouldUpdate(Date lastUpdate) {
+ return !isToday(lastUpdate);
+ }
+
+ public void load(String projectKey, boolean force) {
+ Date lastSync = cacheStatus.getSyncStatus();
+ boolean failOnError = true;
+
+ if (lastSync != null) {
+ if (force) {
+ LOG.info("-- Found project [{}] cache [{}], synchronizing data (forced)..", projectKey, lastSync);
+ } else if (shouldUpdate(lastSync)) {
+ LOG.info("-- Found project [{}] cache [{}], synchronizing data..", projectKey, lastSync);
+ failOnError = false;
+ } else {
+ LOG.info("Found project [{}] cache [{}]", projectKey, lastSync);
+ return;
+ }
+ } else {
+ LOG.info("-- Cache for project [{}] not found, synchronizing data..", projectKey);
+ }
+
+ try {
+ loadData(projectKey);
+ } catch (Exception e) {
+ if (failOnError) {
+ throw e;
+ }
+
+ LOG.warn("-- Cache update for project [{}] failed, continuing from cache..", projectKey, e);
+ return;
+ }
+
+ saveStatus();
+ }
+
+ private void saveStatus() {
+ cacheStatus.save();
+ LOG.info("-- Successfully synchronized project cache");
+ }
+
+ private void loadData(String projectKey) {
+ Profiler profiler = Profiler.create(Loggers.get(ProjectCacheSynchronizer.class));
+
+ profiler.startInfo("Load rules");
+ rulesLoader.load(null);
+ profiler.stopInfo();
+
+ profiler.startInfo("Load project settings");
+ ProjectRepositories projectRepo = projectRepositoriesLoader.load(projectKey, true, null);
+
+ if (!projectRepo.exists()) {
+ LOG.debug("Project doesn't exist in the server");
+ } else if (projectRepo.lastAnalysisDate() == null) {
+ LOG.debug("No previous analysis found");
+ }
+ profiler.stopInfo();
+
+ profiler.startInfo("Load project quality profiles");
+ Collection<QualityProfile> qProfiles;
+ if (projectRepo.exists()) {
+ qProfiles = qualityProfileLoader.load(projectKey, null, null);
+ } else {
+ qProfiles = qualityProfileLoader.loadDefault(null, null);
+ }
+ profiler.stopInfo();
+
+ profiler.startInfo("Load project active rules");
+ Collection<String> keys = getKeys(qProfiles);
+ for (String k : keys) {
+ activeRulesLoader.load(k, null);
+ }
+ profiler.stopInfo();
+
+ if (projectRepo.lastAnalysisDate() != null) {
+ profiler.startInfo("Load server issues");
+ UserLoginAccumulator consumer = new UserLoginAccumulator();
+ issuesLoader.load(projectKey, consumer);
+ profiler.stopInfo();
+
+ profiler.startInfo("Load user information");
+ for (String login : consumer.loginSet) {
+ userRepository.load(login, null);
+ }
+ profiler.stopInfo("Load user information");
+ }
+ }
+
+ private static Collection<String> getKeys(Collection<QualityProfile> qProfiles) {
+ List<String> list = new ArrayList<>(qProfiles.size());
+ for (QualityProfile qp : qProfiles) {
+ list.add(qp.getKey());
+ }
+
+ return list;
+ }
+
+ private static class UserLoginAccumulator implements Function<ServerIssue, Void> {
+ Set<String> loginSet = new HashSet<>();
+
+ @Override
+ public Void apply(ServerIssue input) {
+ if (!StringUtils.isEmpty(input.getAssigneeLogin())) {
+ loginSet.add(input.getAssigneeLogin());
+ }
+ return null;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java
new file mode 100644
index 00000000000..abbd8a69485
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import org.sonar.api.batch.bootstrap.ProjectKey;
+
+public class ProjectKeySupplier implements ProjectKey {
+ private final String key;
+
+ ProjectKeySupplier(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public String get() {
+ return key;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java
new file mode 100644
index 00000000000..2594a006543
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import org.sonar.api.batch.bootstrap.ProjectKey;
+
+import org.sonar.batch.util.BatchUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import com.google.common.base.Preconditions;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.Slf4jLogger;
+
+import java.nio.file.Paths;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.home.cache.PersistentCache;
+import org.sonar.home.cache.PersistentCacheBuilder;
+
+public class ProjectPersistentCacheProvider extends ProviderAdapter {
+ private PersistentCache cache;
+
+ public PersistentCache provide(GlobalProperties props, DefaultAnalysisMode mode, ProjectKey key) {
+ if (cache == null) {
+ PersistentCacheBuilder builder = new PersistentCacheBuilder(new Slf4jLogger());
+ String projectKey = key.get();
+ String home = props.property("sonar.userHome");
+ String serverUrl = getServerUrl(props);
+
+ if (home != null) {
+ builder.setSonarHome(Paths.get(home));
+ }
+
+ if (mode.isNotAssociated()) {
+ builder.setAreaForLocalProject(serverUrl, BatchUtils.getServerVersion());
+ } else {
+ Preconditions.checkNotNull(projectKey);
+ builder.setAreaForProject(serverUrl, BatchUtils.getServerVersion(), projectKey);
+ }
+
+ cache = builder.build();
+ }
+
+ return cache;
+ }
+
+ private static String getServerUrl(GlobalProperties props) {
+ return StringUtils.removeEnd(StringUtils.defaultIfBlank(props.property("sonar.host.url"), "http://localhost:9000"), "/");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java
new file mode 100644
index 00000000000..816091786b3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.api.CoreProperties;
+import org.sonar.batch.analysis.AnalysisProperties;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.batch.repository.DefaultProjectRepositoriesLoader;
+import org.sonar.batch.repository.DefaultQualityProfileLoader;
+import org.sonar.batch.repository.DefaultServerIssuesLoader;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
+import org.sonar.batch.repository.QualityProfileLoader;
+import org.sonar.batch.repository.ServerIssuesLoader;
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.batch.rule.ActiveRulesLoader;
+import org.sonar.batch.rule.DefaultActiveRulesLoader;
+import org.sonar.batch.rule.DefaultRulesLoader;
+import org.sonar.batch.rule.RulesLoader;
+import org.sonar.core.platform.ComponentContainer;
+
+public class ProjectSyncContainer extends ComponentContainer {
+ private final boolean force;
+ private final String projectKey;
+
+ public ProjectSyncContainer(ComponentContainer globalContainer, @Nullable String projectKey, boolean force) {
+ super(globalContainer);
+ this.projectKey = projectKey;
+ this.force = force;
+ }
+
+ @Override
+ public void doBeforeStart() {
+ addComponents();
+ }
+
+ @Override
+ public void doAfterStart() {
+ if (projectKey != null) {
+ getComponentByType(ProjectCacheSynchronizer.class).load(projectKey, force);
+ } else {
+ getComponentByType(NonAssociatedCacheSynchronizer.class).execute(force);
+ }
+ }
+
+ private static DefaultAnalysisMode createIssuesAnalysisMode(@Nullable String projectKey) {
+ Map<String, String> props = new HashMap<>();
+ props.put(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES);
+ if (projectKey != null) {
+ props.put(CoreProperties.PROJECT_KEY_PROPERTY, projectKey);
+ }
+ GlobalProperties globalProps = new GlobalProperties(props);
+ AnalysisProperties analysisProps = new AnalysisProperties(props);
+ return new DefaultAnalysisMode(globalProps, analysisProps);
+ }
+
+ private void addComponents() {
+ add(new StrategyWSLoaderProvider(LoadStrategy.SERVER_ONLY),
+ new ProjectKeySupplier(projectKey),
+ projectKey != null ? ProjectCacheSynchronizer.class : NonAssociatedCacheSynchronizer.class,
+ UserRepositoryLoader.class,
+ new ProjectPersistentCacheProvider(),
+ createIssuesAnalysisMode(projectKey));
+
+ addIfMissing(DefaultProjectCacheStatus.class, ProjectCacheStatus.class);
+ addIfMissing(DefaultProjectRepositoriesLoader.class, ProjectRepositoriesLoader.class);
+ addIfMissing(DefaultServerIssuesLoader.class, ServerIssuesLoader.class);
+ addIfMissing(DefaultQualityProfileLoader.class, QualityProfileLoader.class);
+ addIfMissing(DefaultRulesLoader.class, RulesLoader.class);
+ addIfMissing(DefaultActiveRulesLoader.class, ActiveRulesLoader.class);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/StrategyWSLoaderProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/StrategyWSLoaderProvider.java
new file mode 100644
index 00000000000..d89de5ba448
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/StrategyWSLoaderProvider.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.home.cache.PersistentCache;
+
+public class StrategyWSLoaderProvider extends ProviderAdapter {
+ private final LoadStrategy strategy;
+ private WSLoader wsLoader;
+
+ public StrategyWSLoaderProvider(LoadStrategy strategy) {
+ this.strategy = strategy;
+ }
+
+ public WSLoader provide(PersistentCache cache, BatchWsClient client) {
+ if (wsLoader == null) {
+ wsLoader = new WSLoader(strategy, cache, client);
+ }
+ return wsLoader;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoader.java
new file mode 100644
index 00000000000..328480e68bf
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoader.java
@@ -0,0 +1,240 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.home.cache.PersistentCache;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.HttpException;
+
+import static org.sonar.batch.cache.WSLoader.ServerStatus.ACCESSIBLE;
+import static org.sonar.batch.cache.WSLoader.ServerStatus.NOT_ACCESSIBLE;
+import static org.sonar.batch.cache.WSLoader.ServerStatus.UNKNOWN;
+
+public class WSLoader {
+ private static final Logger LOG = Loggers.get(WSLoader.class);
+ private static final String FAIL_MSG = "Server is not accessible and data is not cached";
+
+ public enum ServerStatus {
+ UNKNOWN, ACCESSIBLE, NOT_ACCESSIBLE
+ }
+
+ public enum LoadStrategy {
+ SERVER_FIRST, CACHE_FIRST, SERVER_ONLY, CACHE_ONLY
+ }
+
+ private final LoadStrategy defautLoadStrategy;
+ private final BatchWsClient wsClient;
+ private final PersistentCache cache;
+ private ServerStatus serverStatus;
+
+ private DataLoader<String> stringServerLoader = new DataLoader<String>() {
+ @Override
+ public String load(String id) throws IOException {
+ GetRequest getRequest = new GetRequest(id);
+ try (Reader reader = wsClient.call(getRequest).contentReader()) {
+ String str = IOUtils.toString(reader);
+ try {
+ cache.put(id, str.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new IllegalStateException("Error saving to WS cache", e);
+ }
+ return str;
+ }
+ }
+ };
+
+ private DataLoader<String> stringCacheLoader = new DataLoader<String>() {
+ @Override
+ public String load(String id) throws IOException {
+ return cache.getString(id);
+ }
+ };
+
+ private DataLoader<InputStream> streamServerLoader = new DataLoader<InputStream>() {
+ @Override
+ public InputStream load(String id) throws IOException {
+ GetRequest getRequest = new GetRequest(id);
+ try (InputStream is = wsClient.call(getRequest).contentStream()) {
+ try {
+ cache.put(id, is);
+ } catch (IOException e) {
+ throw new IllegalStateException("Error saving to WS cache", e);
+ }
+ }
+ return cache.getStream(id);
+ }
+ };
+
+ private DataLoader<InputStream> streamCacheLoader = new DataLoader<InputStream>() {
+ @Override
+ public InputStream load(String id) throws IOException {
+ return cache.getStream(id);
+ }
+ };
+
+ private static class NotAvailableException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public NotAvailableException(String message) {
+ super(message);
+ }
+
+ public NotAvailableException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ public WSLoader(LoadStrategy strategy, PersistentCache cache, BatchWsClient wsClient) {
+ this.defautLoadStrategy = strategy;
+ this.serverStatus = UNKNOWN;
+ this.cache = cache;
+ this.wsClient = wsClient;
+ }
+
+ @Nonnull
+ public WSLoaderResult<InputStream> loadStream(String id) {
+ return load(id, defautLoadStrategy, streamServerLoader, streamCacheLoader);
+ }
+
+ @Nonnull
+ public WSLoaderResult<String> loadString(String id) {
+ return loadString(id, defautLoadStrategy);
+ }
+
+ @Nonnull
+ public WSLoaderResult<String> loadString(String id, WSLoader.LoadStrategy strategy) {
+ return load(id, strategy, stringServerLoader, stringCacheLoader);
+ }
+
+ @Nonnull
+ private <T> WSLoaderResult<T> load(String id, WSLoader.LoadStrategy strategy, DataLoader<T> serverLoader, DataLoader<T> cacheLoader) {
+ switch (strategy) {
+ case CACHE_FIRST:
+ return loadFromCacheFirst(id, cacheLoader, serverLoader);
+ case CACHE_ONLY:
+ return loadFromCacheFirst(id, cacheLoader, null);
+ case SERVER_FIRST:
+ return loadFromServerFirst(id, serverLoader, cacheLoader);
+ case SERVER_ONLY:
+ default:
+ return loadFromServerFirst(id, serverLoader, null);
+ }
+ }
+
+ public LoadStrategy getDefaultStrategy() {
+ return this.defautLoadStrategy;
+ }
+
+ private void switchToOffline() {
+ LOG.debug("server not available - switching to offline mode");
+ serverStatus = NOT_ACCESSIBLE;
+ }
+
+ private void switchToOnline() {
+ serverStatus = ACCESSIBLE;
+ }
+
+ private boolean isOffline() {
+ return serverStatus == NOT_ACCESSIBLE;
+ }
+
+ @Nonnull
+ private <T> WSLoaderResult<T> loadFromCacheFirst(String id, DataLoader<T> cacheLoader, @Nullable DataLoader<T> serverLoader) {
+ try {
+ return loadFromCache(id, cacheLoader);
+ } catch (NotAvailableException cacheNotAvailable) {
+ if (serverLoader != null) {
+ try {
+ return loadFromServer(id, serverLoader);
+ } catch (NotAvailableException serverNotAvailable) {
+ throw new IllegalStateException(FAIL_MSG, serverNotAvailable.getCause());
+ }
+ }
+ throw new IllegalStateException("Data is not cached", cacheNotAvailable.getCause());
+ }
+ }
+
+ @Nonnull
+ private <T> WSLoaderResult<T> loadFromServerFirst(String id, DataLoader<T> serverLoader, @Nullable DataLoader<T> cacheLoader) {
+ try {
+ return loadFromServer(id, serverLoader);
+ } catch (NotAvailableException serverNotAvailable) {
+ if (cacheLoader != null) {
+ try {
+ return loadFromCache(id, cacheLoader);
+ } catch (NotAvailableException cacheNotAvailable) {
+ throw new IllegalStateException(FAIL_MSG, serverNotAvailable.getCause());
+ }
+ }
+ throw new IllegalStateException("Server is not available: " + wsClient.baseUrl(), serverNotAvailable.getCause());
+ }
+ }
+
+ interface DataLoader<T> {
+ T load(String id) throws IOException;
+ }
+
+ private <T> WSLoaderResult<T> loadFromCache(String id, DataLoader<T> loader) throws NotAvailableException {
+ T result;
+
+ try {
+ result = loader.load(id);
+ } catch (IOException e) {
+ // any exception on the cache should fail fast
+ throw new IllegalStateException(e);
+ }
+ if (result == null) {
+ throw new NotAvailableException("resource not cached");
+ }
+ return new WSLoaderResult<>(result, true);
+ }
+
+ private <T> WSLoaderResult<T> loadFromServer(String id, DataLoader<T> loader) throws NotAvailableException {
+ if (isOffline()) {
+ throw new NotAvailableException("Server not available");
+ }
+ try {
+ T t = loader.load(id);
+ switchToOnline();
+ return new WSLoaderResult<>(t, false);
+ } catch (HttpException | MessageException e) {
+ // fail fast if it could connect but there was a application-level error
+ throw e;
+ } catch (IllegalStateException e) {
+ switchToOffline();
+ throw new NotAvailableException(e);
+ } catch (Exception e) {
+ // fail fast
+ throw new IllegalStateException(e);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoaderResult.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoaderResult.java
new file mode 100644
index 00000000000..29db0bcb3a7
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/WSLoaderResult.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import javax.annotation.Nonnull;
+
+public class WSLoaderResult<T> {
+ private T result;
+ private boolean fromCache;
+
+ public WSLoaderResult(T result, boolean fromCache) {
+ this.result = result;
+ this.fromCache = fromCache;
+ }
+
+ @Nonnull
+ public T get() {
+ return result;
+ }
+
+ public boolean isFromCache() {
+ return fromCache;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/package-info.java
new file mode 100644
index 00000000000..3f72a2d38b6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cache/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.cache;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdComponents.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdComponents.java
new file mode 100644
index 00000000000..30f4863783d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdComponents.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+public final class CpdComponents {
+
+ private CpdComponents() {
+ }
+
+ public static List<Class<? extends Object>> all() {
+ return ImmutableList.of(
+ CpdSensor.class,
+ CpdMappings.class,
+ JavaCpdBlockIndexer.class,
+ DefaultCpdBlockIndexer.class);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdExecutor.java
new file mode 100644
index 00000000000..410938bab46
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdExecutor.java
@@ -0,0 +1,217 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.util.ProgressReport;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.detector.suffixtree.SuffixTreeCloneDetectionAlgorithm;
+import org.sonar.duplications.index.CloneGroup;
+import org.sonar.duplications.index.ClonePart;
+import org.sonar.duplications.index.PackedMemoryCloneIndex.ResourceBlocks;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReport.Duplicate;
+import org.sonar.scanner.protocol.output.ScannerReport.Duplication;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static com.google.common.collect.FluentIterable.from;
+
+/**
+ * Runs on the root module, at the end of the project analysis.
+ * It executes copy paste detection involving all files of all modules, which were indexed during sensors execution for each module
+ * by {@link CpdSensor). The sensor is responsible for handling exclusions and block sizes.
+ */
+public class CpdExecutor {
+ private static final Logger LOG = Loggers.get(CpdExecutor.class);
+ // timeout for the computation of duplicates in a file (seconds)
+ private static final int TIMEOUT = 5 * 60;
+ static final int MAX_CLONE_GROUP_PER_FILE = 100;
+ static final int MAX_CLONE_PART_PER_GROUP = 100;
+
+ private final SonarCpdBlockIndex index;
+ private final ReportPublisher publisher;
+ private final BatchComponentCache batchComponentCache;
+ private final Settings settings;
+ private final ExecutorService executorService;
+ private final ProgressReport progressReport;
+ private int count;
+ private int total;
+
+ public CpdExecutor(Settings settings, SonarCpdBlockIndex index, ReportPublisher publisher, BatchComponentCache batchComponentCache) {
+ this.settings = settings;
+ this.index = index;
+ this.publisher = publisher;
+ this.batchComponentCache = batchComponentCache;
+ this.executorService = Executors.newSingleThreadExecutor();
+ this.progressReport = new ProgressReport("CPD computation", TimeUnit.SECONDS.toMillis(10));
+ }
+
+ public void execute() {
+ total = index.noResources();
+ progressReport.start(String.format("Calculating CPD for %d files", total));
+ try {
+ Iterator<ResourceBlocks> it = index.iterator();
+
+ while (it.hasNext()) {
+ ResourceBlocks resourceBlocks = it.next();
+ runCpdAnalysis(resourceBlocks.resourceId(), resourceBlocks.blocks());
+ count++;
+ }
+ progressReport.stop("CPD calculation finished");
+ } catch (Exception e) {
+ progressReport.stop("");
+ throw e;
+ }
+ }
+
+ private void runCpdAnalysis(String resource, final Collection<Block> fileBlocks) {
+ LOG.debug("Detection of duplications for {}", resource);
+
+ BatchComponent component = batchComponentCache.get(resource);
+ if (component == null) {
+ LOG.error("Resource not found in component cache: {}. Skipping CPD computation for it", resource);
+ return;
+ }
+
+ InputFile inputFile = (InputFile) component.inputComponent();
+ progressReport.message(String.format("%d/%d - current file: %s", count, total, inputFile.absolutePath()));
+
+ List<CloneGroup> duplications;
+ Future<List<CloneGroup>> futureResult = null;
+ try {
+ futureResult = executorService.submit(new Callable<List<CloneGroup>>() {
+ @Override
+ public List<CloneGroup> call() throws Exception {
+ return SuffixTreeCloneDetectionAlgorithm.detect(index, fileBlocks);
+ }
+ });
+ duplications = futureResult.get(TIMEOUT, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ LOG.warn("Timeout during detection of duplications for " + inputFile.absolutePath());
+ if (futureResult != null) {
+ futureResult.cancel(true);
+ }
+ return;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail during detection of duplication for " + inputFile.absolutePath(), e);
+ }
+
+ List<CloneGroup> filtered;
+ if (!"java".equalsIgnoreCase(inputFile.language())) {
+ Predicate<CloneGroup> minimumTokensPredicate = DuplicationPredicates.numberOfUnitsNotLessThan(getMinimumTokens(inputFile.language()));
+ filtered = from(duplications).filter(minimumTokensPredicate).toList();
+ } else {
+ filtered = duplications;
+ }
+
+ saveDuplications(component, filtered);
+ }
+
+ @VisibleForTesting
+ /**
+ * Not applicable to Java, as the {@link BlockChunker} that it uses does not record start and end units of each block.
+ * Also, it uses statements instead of tokens.
+ * @param languageKey
+ * @return
+ */
+ int getMinimumTokens(String languageKey) {
+ int minimumTokens = settings.getInt("sonar.cpd." + languageKey + ".minimumTokens");
+ if (minimumTokens == 0) {
+ minimumTokens = 100;
+ }
+
+ return minimumTokens;
+ }
+
+ @VisibleForTesting
+ final void saveDuplications(final BatchComponent component, List<CloneGroup> duplications) {
+ if (duplications.size() > MAX_CLONE_GROUP_PER_FILE) {
+ LOG.warn("Too many duplication groups on file " + component.inputComponent() + ". Keep only the first " + MAX_CLONE_GROUP_PER_FILE +
+ " groups.");
+ }
+ Iterable<org.sonar.scanner.protocol.output.ScannerReport.Duplication> reportDuplications = from(duplications)
+ .limit(MAX_CLONE_GROUP_PER_FILE)
+ .transform(
+ new Function<CloneGroup, ScannerReport.Duplication>() {
+ private final ScannerReport.Duplication.Builder dupBuilder = ScannerReport.Duplication.newBuilder();
+ private final ScannerReport.Duplicate.Builder blockBuilder = ScannerReport.Duplicate.newBuilder();
+
+ @Override
+ public ScannerReport.Duplication apply(CloneGroup input) {
+ return toReportDuplication(component, dupBuilder, blockBuilder, input);
+ }
+
+ });
+ publisher.getWriter().writeComponentDuplications(component.batchId(), reportDuplications);
+ }
+
+ private Duplication toReportDuplication(BatchComponent component, Duplication.Builder dupBuilder, Duplicate.Builder blockBuilder, CloneGroup input) {
+ dupBuilder.clear();
+ ClonePart originBlock = input.getOriginPart();
+ blockBuilder.clear();
+ dupBuilder.setOriginPosition(ScannerReport.TextRange.newBuilder()
+ .setStartLine(originBlock.getStartLine())
+ .setEndLine(originBlock.getEndLine())
+ .build());
+ int clonePartCount = 0;
+ for (ClonePart duplicate : input.getCloneParts()) {
+ if (!duplicate.equals(originBlock)) {
+ clonePartCount++;
+ if (clonePartCount > MAX_CLONE_PART_PER_GROUP) {
+ LOG.warn("Too many duplication references on file " + component.inputComponent() + " for block at line " +
+ originBlock.getStartLine() + ". Keep only the first "
+ + MAX_CLONE_PART_PER_GROUP + " references.");
+ break;
+ }
+ blockBuilder.clear();
+ String componentKey = duplicate.getResourceId();
+ if (!component.key().equals(componentKey)) {
+ BatchComponent sameProjectComponent = batchComponentCache.get(componentKey);
+ blockBuilder.setOtherFileRef(sameProjectComponent.batchId());
+ }
+ dupBuilder.addDuplicate(blockBuilder
+ .setRange(ScannerReport.TextRange.newBuilder()
+ .setStartLine(duplicate.getStartLine())
+ .setEndLine(duplicate.getEndLine())
+ .build())
+ .build());
+ }
+ }
+ return dupBuilder.build();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdIndexer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdIndexer.java
new file mode 100644
index 00000000000..9ebf80c88a7
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdIndexer.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import org.slf4j.Logger;
+import org.sonar.api.batch.BatchSide;
+
+@BatchSide
+public abstract class CpdIndexer {
+
+ abstract boolean isLanguageSupported(String language);
+
+ abstract void index(String language);
+
+ protected void logExclusions(String[] exclusions, Logger logger) {
+ if (exclusions.length > 0) {
+ StringBuilder message = new StringBuilder("Copy-paste detection exclusions:");
+ for (String exclusion : exclusions) {
+ message.append("\n ");
+ message.append(exclusion);
+ }
+
+ logger.info(message.toString());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdMappings.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdMappings.java
new file mode 100644
index 00000000000..9a930d62fb5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdMappings.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.CpdMapping;
+
+import javax.annotation.CheckForNull;
+
+@BatchSide
+public class CpdMappings {
+
+ private final CpdMapping[] mappings;
+
+ public CpdMappings(CpdMapping[] mappings) {
+ this.mappings = mappings;
+ }
+
+ public CpdMappings() {
+ this(new CpdMapping[0]);
+ }
+
+ @CheckForNull
+ public CpdMapping getMapping(String language) {
+ if (mappings != null) {
+ for (CpdMapping cpdMapping : mappings) {
+ if (cpdMapping.getLanguage().getKey().equals(language)) {
+ return cpdMapping;
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdSensor.java
new file mode 100644
index 00000000000..9ea1bac9254
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/CpdSensor.java
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.Phase;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.config.Settings;
+
+@Phase(name = Phase.Name.POST)
+public class CpdSensor implements Sensor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CpdSensor.class);
+
+ private CpdIndexer sonarEngine;
+ private CpdIndexer sonarBridgeEngine;
+ private Settings settings;
+ private FileSystem fs;
+
+ public CpdSensor(JavaCpdBlockIndexer sonarEngine, DefaultCpdBlockIndexer sonarBridgeEngine, Settings settings, FileSystem fs) {
+ this.sonarEngine = sonarEngine;
+ this.sonarBridgeEngine = sonarBridgeEngine;
+ this.settings = settings;
+ this.fs = fs;
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.name("CPD Sensor");
+ }
+
+ @VisibleForTesting
+ CpdIndexer getEngine(String language) {
+ if (sonarEngine.isLanguageSupported(language)) {
+ return sonarEngine;
+ }
+ return sonarBridgeEngine;
+ }
+
+ @VisibleForTesting
+ boolean isSkipped(String language) {
+ String key = "sonar.cpd." + language + ".skip";
+ if (settings.hasKey(key)) {
+ return settings.getBoolean(key);
+ }
+ return settings.getBoolean(CoreProperties.CPD_SKIP_PROPERTY);
+ }
+
+ @Override
+ public void execute(SensorContext context) {
+ if (settings.hasKey(CoreProperties.CPD_SKIP_PROPERTY)) {
+ LOG.warn("\"sonar.cpd.skip\" property is deprecated and will be removed. Please set \"sonar.cpd.exclusions=**\" instead to disable duplication mechanism.");
+ }
+
+ for (String language : fs.languages()) {
+ if (settings.hasKey("sonar.cpd." + language + ".skip")) {
+ LOG
+ .warn("\"sonar.cpd." + language + ".skip\" property is deprecated and will be removed. Please set \"sonar.cpd.exclusions=**\" instead to disable duplication mechanism.");
+ }
+
+ if (isSkipped(language)) {
+ LOG.info("Detection of duplicated code is skipped for {}", language);
+ continue;
+ }
+
+ CpdIndexer engine = getEngine(language);
+ if (!engine.isLanguageSupported(language)) {
+ LOG.debug("Detection of duplicated code is not supported for {}", language);
+ continue;
+ }
+ LOG.info("{} is used for {}", engine, language);
+ engine.index(language);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DefaultCpdBlockIndexer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DefaultCpdBlockIndexer.java
new file mode 100644
index 00000000000..33b2f269595
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DefaultCpdBlockIndexer.java
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.CpdMapping;
+import org.sonar.api.batch.fs.FilePredicates;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.internal.pmd.TokenizerBridge;
+
+public class DefaultCpdBlockIndexer extends CpdIndexer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultCpdBlockIndexer.class);
+
+ private final CpdMappings mappings;
+ private final FileSystem fs;
+ private final Settings settings;
+ private final SonarCpdBlockIndex index;
+
+ public DefaultCpdBlockIndexer(CpdMappings mappings, FileSystem fs, Settings settings, SonarCpdBlockIndex index) {
+ this.mappings = mappings;
+ this.fs = fs;
+ this.settings = settings;
+ this.index = index;
+ }
+
+ @Override
+ public boolean isLanguageSupported(String language) {
+ return true;
+ }
+
+ @Override
+ public void index(String languageKey) {
+ CpdMapping mapping = mappings.getMapping(languageKey);
+ if (mapping == null) {
+ LOG.debug("No CpdMapping for language " + languageKey);
+ return;
+ }
+
+ String[] cpdExclusions = settings.getStringArray(CoreProperties.CPD_EXCLUSIONS);
+ logExclusions(cpdExclusions, LOG);
+ FilePredicates p = fs.predicates();
+ List<InputFile> sourceFiles = Lists.newArrayList(fs.inputFiles(p.and(
+ p.hasType(InputFile.Type.MAIN),
+ p.hasLanguage(languageKey),
+ p.doesNotMatchPathPatterns(cpdExclusions))));
+ if (sourceFiles.isEmpty()) {
+ return;
+ }
+
+ // Create index
+ populateIndex(languageKey, sourceFiles, mapping);
+ }
+
+ private void populateIndex(String languageKey, List<InputFile> sourceFiles, CpdMapping mapping) {
+ TokenizerBridge bridge = new TokenizerBridge(mapping.getTokenizer(), fs.encoding().name(), getBlockSize(languageKey));
+ for (InputFile inputFile : sourceFiles) {
+ if (!index.isIndexed(inputFile)) {
+ LOG.debug("Populating index from {}", inputFile);
+ String resourceEffectiveKey = ((DefaultInputFile) inputFile).key();
+ List<Block> blocks = bridge.chunk(resourceEffectiveKey, inputFile.file());
+ index.insert(inputFile, blocks);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ int getBlockSize(String languageKey) {
+ int blockSize = settings.getInt("sonar.cpd." + languageKey + ".minimumLines");
+ if (blockSize == 0) {
+ blockSize = getDefaultBlockSize(languageKey);
+ }
+ return blockSize;
+ }
+
+ @VisibleForTesting
+ public static int getDefaultBlockSize(String languageKey) {
+ if ("cobol".equals(languageKey)) {
+ return 30;
+ } else if ("abap".equals(languageKey)) {
+ return 20;
+ } else {
+ return 10;
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DuplicationPredicates.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DuplicationPredicates.java
new file mode 100644
index 00000000000..e016ae6f970
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/DuplicationPredicates.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import com.google.common.base.Predicate;
+import org.sonar.duplications.index.CloneGroup;
+
+import javax.annotation.Nullable;
+
+public final class DuplicationPredicates {
+
+ private DuplicationPredicates() {
+ }
+
+ public static Predicate<CloneGroup> numberOfUnitsNotLessThan(int min) {
+ return new NumberOfUnitsNotLessThan(min);
+ }
+
+ private static class NumberOfUnitsNotLessThan implements Predicate<CloneGroup> {
+ private final int min;
+
+ public NumberOfUnitsNotLessThan(int min) {
+ this.min = min;
+ }
+
+ @Override
+ public boolean apply(@Nullable CloneGroup input) {
+ return input != null && input.getLengthInUnits() >= min;
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/JavaCpdBlockIndexer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/JavaCpdBlockIndexer.java
new file mode 100644
index 00000000000..a2dde817c74
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/JavaCpdBlockIndexer.java
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import com.google.common.collect.Lists;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.FilePredicates;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.BlockChunker;
+import org.sonar.duplications.java.JavaStatementBuilder;
+import org.sonar.duplications.java.JavaTokenProducer;
+import org.sonar.duplications.statement.Statement;
+import org.sonar.duplications.statement.StatementChunker;
+import org.sonar.duplications.token.TokenChunker;
+
+public class JavaCpdBlockIndexer extends CpdIndexer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JavaCpdBlockIndexer.class);
+
+ private static final int BLOCK_SIZE = 10;
+
+ private final FileSystem fs;
+ private final Settings settings;
+ private final SonarCpdBlockIndex index;
+
+ public JavaCpdBlockIndexer(FileSystem fs, Settings settings, SonarCpdBlockIndex index) {
+ this.fs = fs;
+ this.settings = settings;
+ this.index = index;
+ }
+
+ @Override
+ public boolean isLanguageSupported(String language) {
+ return "java".equals(language);
+ }
+
+ @Override
+ public void index(String languageKey) {
+ String[] cpdExclusions = settings.getStringArray(CoreProperties.CPD_EXCLUSIONS);
+ logExclusions(cpdExclusions, LOG);
+ FilePredicates p = fs.predicates();
+ List<InputFile> sourceFiles = Lists.newArrayList(fs.inputFiles(p.and(
+ p.hasType(InputFile.Type.MAIN),
+ p.hasLanguage(languageKey),
+ p.doesNotMatchPathPatterns(cpdExclusions))));
+ if (sourceFiles.isEmpty()) {
+ return;
+ }
+ createIndex(sourceFiles);
+ }
+
+ private void createIndex(Iterable<InputFile> sourceFiles) {
+ TokenChunker tokenChunker = JavaTokenProducer.build();
+ StatementChunker statementChunker = JavaStatementBuilder.build();
+ BlockChunker blockChunker = new BlockChunker(BLOCK_SIZE);
+
+ for (InputFile inputFile : sourceFiles) {
+ LOG.debug("Populating index from {}", inputFile);
+ String resourceEffectiveKey = ((DefaultInputFile) inputFile).key();
+
+ List<Statement> statements;
+
+ try(Reader reader = new InputStreamReader(new FileInputStream(inputFile.file()), fs.encoding())) {
+ statements = statementChunker.chunk(tokenChunker.chunk(reader));
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException("Cannot find file " + inputFile.file(), e);
+ } catch (IOException e ) {
+ throw new IllegalStateException("Exception hnadling file: " + inputFile.file(), e);
+ }
+
+ List<Block> blocks = blockChunker.chunk(resourceEffectiveKey, statements);
+ index.insert(inputFile, blocks);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/SonarCpdBlockIndex.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/SonarCpdBlockIndex.java
new file mode 100644
index 00000000000..e819c12e32e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/SonarCpdBlockIndex.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd.index;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.ByteArray;
+import org.sonar.duplications.index.AbstractCloneIndex;
+import org.sonar.duplications.index.CloneIndex;
+import org.sonar.duplications.index.PackedMemoryCloneIndex;
+import org.sonar.duplications.index.PackedMemoryCloneIndex.ResourceBlocks;
+import org.sonar.scanner.protocol.output.ScannerReport;
+
+public class SonarCpdBlockIndex extends AbstractCloneIndex {
+
+ private final CloneIndex mem = new PackedMemoryCloneIndex();
+ private final ReportPublisher publisher;
+ private final BatchComponentCache batchComponentCache;
+ private final Settings settings;
+ // Files already tokenized
+ private final Set<InputFile> indexedFiles = new HashSet<>();
+
+ public SonarCpdBlockIndex(ReportPublisher publisher, BatchComponentCache batchComponentCache, Settings settings) {
+ this.publisher = publisher;
+ this.batchComponentCache = batchComponentCache;
+ this.settings = settings;
+ }
+
+ public void insert(InputFile inputFile, Collection<Block> blocks) {
+ if (isCrossProjectDuplicationEnabled(settings)) {
+ int id = batchComponentCache.get(inputFile).batchId();
+ final ScannerReport.CpdTextBlock.Builder builder = ScannerReport.CpdTextBlock.newBuilder();
+ publisher.getWriter().writeCpdTextBlocks(id, Iterables.transform(blocks, new Function<Block, ScannerReport.CpdTextBlock>() {
+ @Override
+ public ScannerReport.CpdTextBlock apply(Block input) {
+ builder.clear();
+ builder.setStartLine(input.getStartLine());
+ builder.setEndLine(input.getEndLine());
+ builder.setStartTokenIndex(input.getStartUnit());
+ builder.setEndTokenIndex(input.getEndUnit());
+ builder.setHash(input.getBlockHash().toHexString());
+ return builder.build();
+ }
+ }));
+ }
+ for (Block block : blocks) {
+ mem.insert(block);
+ }
+ indexedFiles.add(inputFile);
+ }
+
+ public boolean isIndexed(InputFile inputFile) {
+ return indexedFiles.contains(inputFile);
+ }
+
+ public static boolean isCrossProjectDuplicationEnabled(Settings settings) {
+ return settings.getBoolean(CoreProperties.CPD_CROSS_PROJECT)
+ // No cross project duplication for branches
+ && StringUtils.isBlank(settings.getString(CoreProperties.PROJECT_BRANCH_PROPERTY));
+ }
+
+ public Collection<Block> getByInputFile(String resourceKey) {
+ return mem.getByResourceId(resourceKey);
+ }
+
+ @Override
+ public Collection<Block> getBySequenceHash(ByteArray hash) {
+ return mem.getBySequenceHash(hash);
+ }
+
+ @Override
+ public Collection<Block> getByResourceId(String resourceId) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void insert(Block block) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Iterator<ResourceBlocks> iterator() {
+ return mem.iterator();
+ }
+
+ @Override
+ public int noResources() {
+ return mem.noResources();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/package-info.java
new file mode 100644
index 00000000000..86431b3a529
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/index/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.cpd.index;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/package-info.java
new file mode 100644
index 00000000000..c4205b6d38e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/cpd/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.cpd;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java
new file mode 100644
index 00000000000..33a56f3e5ee
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/DeprecatedSensorContext.java
@@ -0,0 +1,221 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.deprecated;
+
+import java.io.Serializable;
+import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputModule;
+import org.sonar.api.batch.fs.InputPath;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.config.Settings;
+import org.sonar.api.design.Dependency;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.MeasuresFilter;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.utils.SonarException;
+import org.sonar.batch.sensor.DefaultSensorContext;
+import org.sonar.batch.sensor.coverage.CoverageExclusions;
+
+public class DeprecatedSensorContext extends DefaultSensorContext implements SensorContext {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DeprecatedSensorContext.class);
+
+ private final SonarIndex index;
+ private final Project project;
+ private final CoverageExclusions coverageFilter;
+
+ public DeprecatedSensorContext(InputModule module, SonarIndex index, Project project, Settings settings, FileSystem fs, ActiveRules activeRules,
+ AnalysisMode analysisMode, CoverageExclusions coverageFilter,
+ SensorStorage sensorStorage) {
+ super(module, settings, fs, activeRules, analysisMode, sensorStorage);
+ this.index = index;
+ this.project = project;
+ this.coverageFilter = coverageFilter;
+
+ }
+
+ public Project getProject() {
+ return project;
+ }
+
+ @Override
+ public boolean index(Resource resource) {
+ // SONAR-5006
+ logWarning();
+ return true;
+ }
+
+ @Override
+ public boolean index(Resource resource, Resource parentReference) {
+ // SONAR-5006
+ logWarning();
+ return true;
+ }
+
+ private static void logWarning() {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Plugins are no more responsible for indexing physical resources like directories and files. This is now handled by the platform.", new SonarException(
+ "Plugin should not index physical resources"));
+ }
+ }
+
+ @Override
+ public boolean isExcluded(Resource reference) {
+ return index.isExcluded(reference);
+ }
+
+ @Override
+ public boolean isIndexed(Resource reference, boolean acceptExcluded) {
+ return index.isIndexed(reference, acceptExcluded);
+ }
+
+ @Override
+ public Resource getParent(Resource reference) {
+ return index.getParent(reference);
+ }
+
+ @Override
+ public Collection<Resource> getChildren(Resource reference) {
+ return index.getChildren(reference);
+ }
+
+ @Override
+ public <G extends Serializable> Measure<G> getMeasure(Metric<G> metric) {
+ return index.getMeasure(project, metric);
+ }
+
+ @Override
+ public <M> M getMeasures(MeasuresFilter<M> filter) {
+ return index.getMeasures(project, filter);
+ }
+
+ @Override
+ public Measure saveMeasure(Measure measure) {
+ return index.addMeasure(project, measure);
+ }
+
+ @Override
+ public Measure saveMeasure(Metric metric, Double value) {
+ return index.addMeasure(project, new Measure(metric, value));
+ }
+
+ @Override
+ public <G extends Serializable> Measure<G> getMeasure(Resource resource, Metric<G> metric) {
+ return index.getMeasure(resource, metric);
+ }
+
+ @Override
+ public String saveResource(Resource resource) {
+ Resource persistedResource = index.addResource(resource);
+ if (persistedResource != null) {
+ return persistedResource.getEffectiveKey();
+ }
+ return null;
+ }
+
+ public boolean saveResource(Resource resource, Resource parentReference) {
+ return index.index(resource, parentReference);
+ }
+
+ @Override
+ public Resource getResource(Resource resource) {
+ return index.getResource(resource);
+ }
+
+ @Override
+ public <M> M getMeasures(Resource resource, MeasuresFilter<M> filter) {
+ return index.getMeasures(resource, filter);
+ }
+
+ @Override
+ public Measure saveMeasure(Resource resource, Metric metric, Double value) {
+ Measure<?> measure = new Measure(metric, value);
+ coverageFilter.validate(measure, resource.getPath());
+ return saveMeasure(resource, measure);
+ }
+
+ @Override
+ public Measure saveMeasure(Resource resource, Measure measure) {
+ Resource resourceOrProject = resourceOrProject(resource);
+
+ if (coverageFilter.accept(resourceOrProject, measure)) {
+ return index.addMeasure(resourceOrProject, measure);
+ } else {
+ return measure;
+ }
+ }
+
+ @Override
+ public Dependency saveDependency(Dependency dependency) {
+ return null;
+ }
+
+ @Override
+ public void saveSource(Resource reference, String source) {
+ // useless since 4.2.
+ }
+
+ private Resource resourceOrProject(Resource resource) {
+ if (resource == null) {
+ return project;
+ }
+ Resource indexedResource = getResource(resource);
+ return indexedResource != null ? indexedResource : resource;
+ }
+
+ @Override
+ public Measure saveMeasure(InputFile inputFile, Metric metric, Double value) {
+ Measure<?> measure = new Measure(metric, value);
+ coverageFilter.validate(measure, inputFile);
+ return saveMeasure(getResource(inputFile), measure);
+ }
+
+ @Override
+ public Measure saveMeasure(InputFile inputFile, Measure measure) {
+ coverageFilter.validate(measure, inputFile);
+ return saveMeasure(getResource(inputFile), measure);
+ }
+
+ @Override
+ public Resource getResource(InputPath inputPath) {
+ Resource r;
+ if (inputPath instanceof InputDir) {
+ r = Directory.create(((InputDir) inputPath).relativePath());
+ } else if (inputPath instanceof InputFile) {
+ r = File.create(((InputFile) inputPath).relativePath());
+ } else {
+ throw new IllegalArgumentException("Unknow input path type: " + inputPath);
+ }
+ return getResource(r);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/InputFileComponent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/InputFileComponent.java
new file mode 100644
index 00000000000..e8dcc72cb32
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/InputFileComponent.java
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.deprecated;
+
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.component.Component;
+import org.sonar.api.resources.Qualifiers;
+
+public class InputFileComponent implements Component {
+
+ private final DefaultInputFile inputFile;
+
+ public InputFileComponent(DefaultInputFile inputFile) {
+ this.inputFile = inputFile;
+ }
+
+ @Override
+ public String key() {
+ return inputFile.key();
+ }
+
+ @Override
+ public String path() {
+ return inputFile.relativePath();
+ }
+
+ @Override
+ public String name() {
+ return inputFile.file().getName();
+ }
+
+ @Override
+ public String longName() {
+ return inputFile.relativePath();
+ }
+
+ @Override
+ public String qualifier() {
+ return inputFile.type() == Type.MAIN ? Qualifiers.FILE : Qualifiers.UNIT_TEST_FILE;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/package-info.java
new file mode 100644
index 00000000000..9ec5890816a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.deprecated;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/BatchPerspectives.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/BatchPerspectives.java
new file mode 100644
index 00000000000..ebc8465b0d3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/BatchPerspectives.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.deprecated.perspectives;
+
+import com.google.common.collect.Maps;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.batch.fs.InputPath;
+import org.sonar.api.component.Perspective;
+import org.sonar.api.component.ResourcePerspectives;
+import org.sonar.api.resources.Resource;
+import org.sonar.batch.index.BatchComponentCache;
+
+public class BatchPerspectives implements ResourcePerspectives {
+
+ private final Map<Class<?>, PerspectiveBuilder<?>> builders = Maps.newHashMap();
+ private final SonarIndex resourceIndex;
+ private final BatchComponentCache componentCache;
+
+ public BatchPerspectives(PerspectiveBuilder[] builders, SonarIndex resourceIndex, BatchComponentCache componentCache) {
+ this.resourceIndex = resourceIndex;
+ this.componentCache = componentCache;
+ for (PerspectiveBuilder builder : builders) {
+ this.builders.put(builder.getPerspectiveClass(), builder);
+ }
+ }
+
+ @Override
+ @CheckForNull
+ public <P extends Perspective> P as(Class<P> perspectiveClass, Resource resource) {
+ Resource indexedResource = resource;
+ if (resource.getEffectiveKey() == null) {
+ indexedResource = resourceIndex.getResource(resource);
+ }
+ if (indexedResource != null) {
+ PerspectiveBuilder<P> builder = builderFor(perspectiveClass);
+ return builder.loadPerspective(perspectiveClass, componentCache.get(indexedResource));
+ }
+ return null;
+ }
+
+ @Override
+ public <P extends Perspective> P as(Class<P> perspectiveClass, InputPath inputPath) {
+ PerspectiveBuilder<P> builder = builderFor(perspectiveClass);
+ return builder.loadPerspective(perspectiveClass, componentCache.get(inputPath));
+ }
+
+ private <T extends Perspective> PerspectiveBuilder<T> builderFor(Class<T> clazz) {
+ PerspectiveBuilder<T> builder = (PerspectiveBuilder<T>) builders.get(clazz);
+ if (builder == null) {
+ throw new PerspectiveNotFoundException("Perspective class is not registered: " + clazz);
+ }
+ return builder;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilder.java
new file mode 100644
index 00000000000..0f38dc2ed41
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilder.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.deprecated.perspectives;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.component.Perspective;
+import org.sonar.batch.index.BatchComponent;
+
+@BatchSide
+public abstract class PerspectiveBuilder<T extends Perspective> {
+
+ private final Class<T> perspectiveClass;
+
+ protected PerspectiveBuilder(Class<T> perspectiveClass) {
+ this.perspectiveClass = perspectiveClass;
+ }
+
+ public Class<T> getPerspectiveClass() {
+ return perspectiveClass;
+ }
+
+ @CheckForNull
+ public abstract T loadPerspective(Class<T> perspectiveClass, BatchComponent component);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveNotFoundException.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveNotFoundException.java
new file mode 100644
index 00000000000..76dcaf532eb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/PerspectiveNotFoundException.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.deprecated.perspectives;
+
+public class PerspectiveNotFoundException extends RuntimeException {
+ public PerspectiveNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/package-info.java
new file mode 100644
index 00000000000..1496e020dd6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/deprecated/perspectives/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.deprecated.perspectives;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchEvent.java
new file mode 100644
index 00000000000..f168bb4cd7e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchEvent.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.events;
+
+import org.sonar.api.batch.events.EventHandler;
+
+/**
+ * Root of all Sonar Batch events.
+ *
+ * @param <H> handler type
+ */
+public abstract class BatchEvent<H extends EventHandler> {
+
+ protected BatchEvent() {
+ }
+
+ /**
+ * Do not call directly - should be called only by {@link EventBus}.
+ * Typically should be implemented as following: <code>handler.onEvent(this)</code>
+ */
+ protected abstract void dispatch(H handler);
+
+ /**
+ * Returns class of associated handler. Used by {@link EventBus} to dispatch events to the correct handlers.
+ */
+ protected abstract Class getType();
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepEvent.java
new file mode 100644
index 00000000000..cfa628edbf0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepEvent.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.events;
+
+import org.sonar.batch.phases.AbstractPhaseEvent;
+
+/**
+ * Generic event for some steps of project scan.
+ * @since 3.7
+ *
+ */
+public class BatchStepEvent extends AbstractPhaseEvent<BatchStepHandler>
+ implements BatchStepHandler.BatchStepEvent {
+
+ private String stepName;
+
+ public BatchStepEvent(String stepName, boolean start) {
+ super(start);
+ this.stepName = stepName;
+ }
+
+ @Override
+ public String stepName() {
+ return stepName;
+ }
+
+ @Override
+ protected void dispatch(BatchStepHandler handler) {
+ handler.onBatchStep(this);
+ }
+
+ @Override
+ protected Class getType() {
+ return BatchStepHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepHandler.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepHandler.java
new file mode 100644
index 00000000000..9d2cc5e54ba
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/BatchStepHandler.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.events;
+
+import org.sonar.api.batch.events.EventHandler;
+
+/**
+ * @since 3.7
+ */
+public interface BatchStepHandler extends EventHandler {
+
+ /**
+ * This interface is not intended to be implemented by clients.
+ */
+ interface BatchStepEvent {
+
+ String stepName();
+
+ boolean isStart();
+
+ boolean isEnd();
+
+ }
+
+ /**
+ * Called before and after execution of each final step of project analysis
+ */
+ void onBatchStep(BatchStepEvent event);
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/events/EventBus.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/EventBus.java
new file mode 100644
index 00000000000..4b1629f1444
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/EventBus.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.events;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.batch.events.EventHandler;
+
+import java.util.List;
+
+/**
+ * Dispatches {@link BatchEvent}s. Eases decoupling by allowing objects to interact without having direct dependencies upon one another, and
+ * without requiring event sources to deal with maintaining handler lists.
+ */
+public class EventBus {
+
+ private EventHandler[] registeredHandlers;
+
+ public EventBus(EventHandler[] handlers) {
+ this.registeredHandlers = handlers;
+ }
+
+ /**
+ * Fires the given event.
+ */
+ public void fireEvent(BatchEvent event) {
+ doFireEvent(event);
+ }
+
+ private void doFireEvent(BatchEvent event) {
+ List<EventHandler> handlers = getDispatchList(event.getType());
+ for (EventHandler handler : handlers) {
+ event.dispatch(handler);
+ }
+ }
+
+ private List<EventHandler> getDispatchList(Class<? extends EventHandler> handlerType) {
+ List<EventHandler> result = Lists.newArrayList();
+ for (EventHandler handler : registeredHandlers) {
+ if (handlerType.isAssignableFrom(handler.getClass())) {
+ result.add(handler);
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/events/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/package-info.java
new file mode 100644
index 00000000000..922fe861be7
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/events/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.events;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponent.java
new file mode 100644
index 00000000000..9690c833b1e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponent.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+
+public class BatchComponent {
+
+ private final int batchId;
+ private final Resource r;
+ private final BatchComponent parent;
+ private final Collection<BatchComponent> children = new ArrayList<>();
+ private InputComponent inputComponent;
+
+ public BatchComponent(int batchId, Resource r, @Nullable BatchComponent parent) {
+ this.batchId = batchId;
+ this.r = r;
+ this.parent = parent;
+ if (parent != null) {
+ parent.children.add(this);
+ }
+ }
+
+ public String key() {
+ return r.getEffectiveKey();
+ }
+
+ public int batchId() {
+ return batchId;
+ }
+
+ public Resource resource() {
+ return r;
+ }
+
+ @CheckForNull
+ public BatchComponent parent() {
+ return parent;
+ }
+
+ public Collection<BatchComponent> children() {
+ return children;
+ }
+
+ public boolean isFile() {
+ return this.inputComponent.isFile();
+ }
+
+ public BatchComponent setInputComponent(InputComponent inputComponent) {
+ this.inputComponent = inputComponent;
+ return this;
+ }
+
+ public InputComponent inputComponent() {
+ return inputComponent;
+ }
+
+ public boolean isProjectOrModule() {
+ return ResourceUtils.isProject(r);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponentCache.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponentCache.java
new file mode 100644
index 00000000000..68c29f69c95
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/BatchComponentCache.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import java.util.Collection;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.resources.Resource;
+
+@BatchSide
+public class BatchComponentCache {
+ // components by key
+ private final Map<String, BatchComponent> components = Maps.newLinkedHashMap();
+
+ private BatchComponent root;
+
+ @CheckForNull
+ public BatchComponent get(String componentKey) {
+ return components.get(componentKey);
+ }
+
+ public BatchComponent get(Resource resource) {
+ return components.get(resource.getEffectiveKey());
+ }
+
+ public BatchComponent get(InputComponent inputComponent) {
+ return components.get(inputComponent.key());
+ }
+
+ public BatchComponent add(Resource resource, @Nullable Resource parentResource) {
+ String componentKey = resource.getEffectiveKey();
+ Preconditions.checkState(!Strings.isNullOrEmpty(componentKey), "Missing resource effective key");
+ BatchComponent parent = parentResource != null ? get(parentResource.getEffectiveKey()) : null;
+ BatchComponent batchComponent = new BatchComponent(components.size() + 1, resource, parent);
+ components.put(componentKey, batchComponent);
+ if (parent == null) {
+ root = batchComponent;
+ }
+ return batchComponent;
+ }
+
+ public Collection<BatchComponent> all() {
+ return components.values();
+ }
+
+ public BatchComponent getRoot() {
+ return root;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Bucket.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Bucket.java
new file mode 100644
index 00000000000..5fde268c097
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Bucket.java
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.resources.Resource;
+
+import javax.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+public final class Bucket {
+
+ private Resource resource;
+
+ private Bucket parent;
+ private List<Bucket> children;
+
+ public Bucket(Resource resource) {
+ this.resource = resource;
+ }
+
+ public Resource getResource() {
+ return resource;
+ }
+
+ public Bucket setParent(@Nullable Bucket parent) {
+ this.parent = parent;
+ if (parent != null) {
+ parent.addChild(this);
+ }
+ return this;
+ }
+
+ private Bucket addChild(Bucket child) {
+ if (children == null) {
+ children = Lists.newArrayList();
+ }
+ children.add(child);
+ return this;
+ }
+
+ private void removeChild(Bucket child) {
+ if (children != null) {
+ children.remove(child);
+ }
+ }
+
+ public List<Bucket> getChildren() {
+ return children == null ? Collections.<Bucket>emptyList() : children;
+ }
+
+ public Bucket getParent() {
+ return parent;
+ }
+
+ public void clear() {
+ children = null;
+ if (parent != null) {
+ parent.removeChild(this);
+ parent = null;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Bucket that = (Bucket) o;
+ return resource.equals(that.resource);
+ }
+
+ @Override
+ public int hashCode() {
+ return resource.hashCode();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Cache.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Cache.java
new file mode 100644
index 00000000000..ad194c3aad0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Cache.java
@@ -0,0 +1,505 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import com.google.common.collect.Sets;
+import com.persistit.Exchange;
+import com.persistit.Key;
+import com.persistit.KeyFilter;
+import com.persistit.exception.PersistitException;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import javax.annotation.CheckForNull;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * <p>
+ * This cache is not thread-safe, due to direct usage of {@link com.persistit.Exchange}
+ * </p>
+ */
+public class Cache<V> {
+
+ private final String name;
+ private final Exchange exchange;
+
+ Cache(String name, Exchange exchange) {
+ this.name = name;
+ this.exchange = exchange;
+ }
+
+ public Cache<V> put(Object key, V value) {
+ resetKey(key);
+ return doPut(value);
+ }
+
+ public Cache<V> put(Object firstKey, Object secondKey, V value) {
+ resetKey(firstKey, secondKey);
+ return doPut(value);
+ }
+
+ public Cache<V> put(Object firstKey, Object secondKey, Object thirdKey, V value) {
+ resetKey(firstKey, secondKey, thirdKey);
+ return doPut(value);
+ }
+
+ public Cache<V> put(Object[] key, V value) {
+ resetKey(key);
+ return doPut(value);
+ }
+
+ private Cache<V> doPut(V value) {
+ try {
+ exchange.getValue().put(value);
+ exchange.store();
+ return this;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to put element in the cache " + name, e);
+ }
+ }
+
+ /**
+ * Returns the value object associated with keys, or null if not found.
+ */
+ public V get(Object key) {
+ resetKey(key);
+ return doGet();
+ }
+
+ /**
+ * Returns the value object associated with keys, or null if not found.
+ */
+ @CheckForNull
+ public V get(Object firstKey, Object secondKey) {
+ resetKey(firstKey, secondKey);
+ return doGet();
+ }
+
+ /**
+ * Returns the value object associated with keys, or null if not found.
+ */
+ @CheckForNull
+ public V get(Object firstKey, Object secondKey, Object thirdKey) {
+ resetKey(firstKey, secondKey, thirdKey);
+ return doGet();
+ }
+
+ /**
+ * Returns the value object associated with keys, or null if not found.
+ */
+ @CheckForNull
+ public V get(Object[] key) {
+ resetKey(key);
+ return doGet();
+ }
+
+ @SuppressWarnings("unchecked")
+ @CheckForNull
+ private V doGet() {
+ try {
+ exchange.fetch();
+ if (!exchange.getValue().isDefined()) {
+ return null;
+ }
+ return (V) exchange.getValue().get();
+ } catch (Exception e) {
+ // TODO add parameters to message
+ throw new IllegalStateException("Fail to get element from cache " + name, e);
+ }
+ }
+
+ public boolean containsKey(Object key) {
+ resetKey(key);
+ return doContainsKey();
+ }
+
+ public boolean containsKey(Object firstKey, Object secondKey) {
+ resetKey(firstKey, secondKey);
+ return doContainsKey();
+ }
+
+ public boolean containsKey(Object firstKey, Object secondKey, Object thirdKey) {
+ resetKey(firstKey, secondKey, thirdKey);
+ return doContainsKey();
+ }
+
+ public boolean containsKey(Object[] key) {
+ resetKey(key);
+ return doContainsKey();
+ }
+
+ private boolean doContainsKey() {
+ try {
+ exchange.fetch();
+ return exchange.isValueDefined();
+ } catch (Exception e) {
+ // TODO add parameters to message
+ throw new IllegalStateException("Fail to check if element is in cache " + name, e);
+ }
+ }
+
+ public boolean remove(Object key) {
+ resetKey(key);
+ return doRemove();
+ }
+
+ public boolean remove(Object firstKey, Object secondKey) {
+ resetKey(firstKey, secondKey);
+ return doRemove();
+ }
+
+ public boolean remove(Object firstKey, Object secondKey, Object thirdKey) {
+ resetKey(firstKey, secondKey, thirdKey);
+ return doRemove();
+ }
+
+ public boolean remove(Object[] key) {
+ resetKey(key);
+ return doRemove();
+ }
+
+ private boolean doRemove() {
+ try {
+ return exchange.remove();
+ } catch (Exception e) {
+ // TODO add parameters to message
+ throw new IllegalStateException("Fail to get element from cache " + name, e);
+ }
+ }
+
+ /**
+ * Removes everything in the specified group.
+ *
+ * @param group The group name.
+ */
+ public Cache<V> clear(Object key) {
+ resetKey(key);
+ return doClear();
+ }
+
+ public Cache<V> clear(Object firstKey, Object secondKey) {
+ resetKey(firstKey, secondKey);
+ return doClear();
+ }
+
+ public Cache<V> clear(Object firstKey, Object secondKey, Object thirdKey) {
+ resetKey(firstKey, secondKey, thirdKey);
+ return doClear();
+ }
+
+ public Cache<V> clear(Object[] key) {
+ resetKey(key);
+ return doClear();
+ }
+
+ private Cache<V> doClear() {
+ try {
+ Key to = new Key(exchange.getKey());
+ to.append(Key.AFTER);
+ exchange.removeKeyRange(exchange.getKey(), to);
+ return this;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to clear values from cache " + name, e);
+ }
+ }
+
+ /**
+ * Clears the default as well as all group caches.
+ */
+ public void clear() {
+ try {
+ exchange.clear();
+ exchange.removeAll();
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to clear cache", e);
+ }
+ }
+
+ /**
+ * Returns the set of cache keys associated with this group.
+ * TODO implement a lazy-loading equivalent with Iterator/Iterable
+ *
+ * @param group The group.
+ * @return The set of cache keys for this group.
+ */
+ @SuppressWarnings("rawtypes")
+ public Set keySet(Object key) {
+ try {
+ Set<Object> keys = Sets.newLinkedHashSet();
+ exchange.clear();
+ Exchange iteratorExchange = new Exchange(exchange);
+ iteratorExchange.append(key);
+ iteratorExchange.append(Key.BEFORE);
+ while (iteratorExchange.next(false)) {
+ keys.add(iteratorExchange.getKey().indexTo(-1).decode());
+ }
+ return keys;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to get keys from cache " + name, e);
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Set keySet(Object firstKey, Object secondKey) {
+ try {
+ Set<Object> keys = Sets.newLinkedHashSet();
+ exchange.clear();
+ Exchange iteratorExchange = new Exchange(exchange);
+ iteratorExchange.append(firstKey);
+ iteratorExchange.append(secondKey);
+ iteratorExchange.append(Key.BEFORE);
+ while (iteratorExchange.next(false)) {
+ keys.add(iteratorExchange.getKey().indexTo(-1).decode());
+ }
+ return keys;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to get keys from cache " + name, e);
+ }
+ }
+
+ /**
+ * Returns the set of keys associated with this cache.
+ *
+ * @return The set containing the keys for this cache.
+ */
+ public Set<Object> keySet() {
+ try {
+ Set<Object> keys = Sets.newLinkedHashSet();
+ exchange.clear();
+ Exchange iteratorExchange = new Exchange(exchange);
+ iteratorExchange.append(Key.BEFORE);
+ while (iteratorExchange.next(false)) {
+ keys.add(iteratorExchange.getKey().indexTo(-1).decode());
+ }
+ return keys;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to get keys from cache " + name, e);
+ }
+ }
+
+ /**
+ * Lazy-loading values for given keys
+ */
+ public Iterable<V> values(Object firstKey, Object secondKey) {
+ return new ValueIterable<>(exchange, firstKey, secondKey);
+ }
+
+ /**
+ * Lazy-loading values for a given key
+ */
+ public Iterable<V> values(Object firstKey) {
+ return new ValueIterable<>(exchange, firstKey);
+ }
+
+ /**
+ * Lazy-loading values
+ */
+ public Iterable<V> values() {
+ return new ValueIterable<>(exchange);
+ }
+
+ public Iterable<Entry<V>> entries() {
+ return new EntryIterable<>(exchange);
+ }
+
+ public Iterable<Entry<V>> entries(Object firstKey) {
+ return new EntryIterable<>(exchange, firstKey);
+ }
+
+ private void resetKey(Object key) {
+ exchange.clear();
+ exchange.append(key);
+ }
+
+ private void resetKey(Object first, Object second) {
+ exchange.clear();
+ exchange.append(first).append(second);
+ }
+
+ private void resetKey(Object first, Object second, Object third) {
+ exchange.clear();
+ exchange.append(first).append(second).append(third);
+ }
+
+ private void resetKey(Object[] keys) {
+ exchange.clear();
+ for (Object o : keys) {
+ exchange.append(o);
+ }
+ }
+
+ //
+ // LAZY ITERATORS AND ITERABLES
+ //
+
+ private static class ValueIterable<T> implements Iterable<T> {
+ private final Exchange originExchange;
+ private final Object[] keys;
+
+ private ValueIterable(Exchange originExchange, Object... keys) {
+ this.originExchange = originExchange;
+ this.keys = keys;
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ originExchange.clear();
+ KeyFilter filter = new KeyFilter();
+ for (Object key : keys) {
+ originExchange.append(key);
+ filter = filter.append(KeyFilter.simpleTerm(key));
+ }
+ originExchange.append(Key.BEFORE);
+ Exchange iteratorExchange = new Exchange(originExchange);
+ return new ValueIterator<>(iteratorExchange, filter);
+ }
+ }
+
+ private static class ValueIterator<T> implements Iterator<T> {
+ private final Exchange exchange;
+ private final KeyFilter keyFilter;
+
+ private ValueIterator(Exchange exchange, KeyFilter keyFilter) {
+ this.exchange = exchange;
+ this.keyFilter = keyFilter;
+ }
+
+ @Override
+ public boolean hasNext() {
+ try {
+ return exchange.hasNext(keyFilter);
+ } catch (PersistitException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T next() {
+ try {
+ exchange.next(keyFilter);
+ } catch (PersistitException e) {
+ throw new IllegalStateException(e);
+ }
+ if (exchange.getValue().isDefined()) {
+ return (T) exchange.getValue().get();
+ }
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Removing an item is not supported");
+ }
+ }
+
+ private static class EntryIterable<T> implements Iterable<Entry<T>> {
+ private final Exchange originExchange;
+ private final Object[] keys;
+
+ private EntryIterable(Exchange originExchange, Object... keys) {
+ this.originExchange = originExchange;
+ this.keys = keys;
+ }
+
+ @Override
+ public Iterator<Entry<T>> iterator() {
+ originExchange.clear();
+ KeyFilter filter = new KeyFilter();
+ for (Object key : keys) {
+ originExchange.append(key);
+ filter = filter.append(KeyFilter.simpleTerm(key));
+ }
+ originExchange.append(Key.BEFORE);
+ Exchange iteratorExchange = new Exchange(originExchange);
+ return new EntryIterator<>(iteratorExchange, filter);
+ }
+ }
+
+ private static class EntryIterator<T> implements Iterator<Entry<T>> {
+ private final Exchange exchange;
+ private final KeyFilter keyFilter;
+
+ private EntryIterator(Exchange exchange, KeyFilter keyFilter) {
+ this.exchange = exchange;
+ this.keyFilter = keyFilter;
+ }
+
+ @Override
+ public boolean hasNext() {
+ try {
+ return exchange.hasNext(keyFilter);
+ } catch (PersistitException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Entry<T> next() {
+ try {
+ exchange.next(keyFilter);
+ } catch (PersistitException e) {
+ throw new IllegalStateException(e);
+ }
+ if (exchange.getValue().isDefined()) {
+ T value = (T) exchange.getValue().get();
+ Key key = exchange.getKey();
+ Object[] array = new Object[key.getDepth()];
+ for (int i = 0; i < key.getDepth(); i++) {
+ array[i] = key.indexTo(i - key.getDepth()).decode();
+ }
+ return new Entry<>(array, value);
+ }
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Removing an item is not supported");
+ }
+ }
+
+ public static class Entry<V> {
+ private final Object[] key;
+ private final V value;
+
+ Entry(Object[] key, V value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public Object[] key() {
+ return key;
+ }
+
+ public V value() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Caches.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Caches.java
new file mode 100644
index 00000000000..ddddb271dfd
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/Caches.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.google.common.base.Preconditions;
+import com.persistit.Exchange;
+import com.persistit.Value;
+import com.persistit.encoding.CoderManager;
+import com.persistit.Persistit;
+import com.persistit.encoding.ValueCoder;
+import com.persistit.exception.PersistitException;
+import com.persistit.Volume;
+import org.picocontainer.Startable;
+import org.sonar.api.batch.BatchSide;
+
+@BatchSide
+public class Caches implements Startable {
+ private final Map<String, Exchange> cacheMap = Maps.newHashMap();
+ private Persistit persistit;
+ private Volume volume;
+
+ public Caches(CachesManager caches) {
+ persistit = caches.persistit();
+ doStart();
+ }
+
+ @Override
+ public void start() {
+ // done in constructor
+ }
+
+ private void doStart() {
+ try {
+ persistit.flush();
+ volume = persistit.createTemporaryVolume();
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to create a cache volume", e);
+ }
+ }
+
+ public void registerValueCoder(Class<?> clazz, ValueCoder coder) {
+ CoderManager cm = persistit.getCoderManager();
+ cm.registerValueCoder(clazz, coder);
+ }
+
+ public <V> Cache<V> createCache(String cacheName) {
+ Preconditions.checkState(volume != null && volume.isOpened(), "Caches are not initialized");
+ Preconditions.checkState(!cacheMap.containsKey(cacheName), "Cache is already created: " + cacheName);
+ try {
+ Exchange exchange = persistit.getExchange(volume, cacheName, true);
+ exchange.setMaximumValueSize(Value.MAXIMUM_SIZE);
+ Cache<V> cache = new Cache<>(cacheName, exchange);
+ cacheMap.put(cacheName, exchange);
+ return cache;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to create cache: " + cacheName, e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ for (Entry<String, Exchange> e : cacheMap.entrySet()) {
+ persistit.releaseExchange(e.getValue());
+ }
+
+ cacheMap.clear();
+
+ if (volume != null) {
+ try {
+ volume.close();
+ volume.delete();
+ } catch (PersistitException e) {
+ throw new IllegalStateException("Fail to close caches", e);
+ }
+ volume = null;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/CachesManager.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/CachesManager.java
new file mode 100644
index 00000000000..010375a84eb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/CachesManager.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import com.persistit.Persistit;
+import com.persistit.exception.PersistitException;
+import com.persistit.logging.Slf4jAdapter;
+import java.io.File;
+import java.util.Properties;
+import org.picocontainer.Startable;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.utils.TempFolder;
+
+import static org.sonar.core.util.FileUtils.deleteQuietly;
+
+/**
+ * Factory of caches
+ *
+ * @since 3.6
+ */
+@BatchSide
+public class CachesManager implements Startable {
+ private File tempDir;
+ private Persistit persistit;
+ private final TempFolder tempFolder;
+
+ public CachesManager(TempFolder tempFolder) {
+ this.tempFolder = tempFolder;
+ initPersistit();
+ }
+
+ private void initPersistit() {
+ try {
+ tempDir = tempFolder.newDir("caches");
+ persistit = new Persistit();
+ persistit.setPersistitLogger(new Slf4jAdapter(LoggerFactory.getLogger("PERSISTIT")));
+ Properties props = new Properties();
+ props.setProperty("datapath", tempDir.getAbsolutePath());
+ props.setProperty("logpath", "${datapath}/log");
+ props.setProperty("logfile", "${logpath}/persistit_${timestamp}.log");
+ props.setProperty("buffer.count.8192", "10");
+ props.setProperty("journalpath", "${datapath}/journal");
+ props.setProperty("tmpvoldir", "${datapath}");
+ props.setProperty("volume.1", "${datapath}/persistit,create,pageSize:8192,initialPages:10,extensionPages:100,maximumPages:25000");
+ props.setProperty("jmx", "false");
+ persistit.setProperties(props);
+ persistit.initialize();
+
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to start caches", e);
+ }
+ }
+
+ @Override
+ public void start() {
+ // already started in constructor
+ }
+
+ @Override
+ public void stop() {
+ if (persistit != null) {
+ try {
+ persistit.close(false);
+ persistit = null;
+ } catch (PersistitException e) {
+ throw new IllegalStateException("Fail to close caches", e);
+ }
+ }
+ deleteQuietly(tempDir);
+ tempDir = null;
+ }
+
+ File tempDir() {
+ return tempDir;
+ }
+
+ Persistit persistit() {
+ return persistit;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/DefaultIndex.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/DefaultIndex.java
new file mode 100644
index 00000000000..d5f1a561729
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/DefaultIndex.java
@@ -0,0 +1,353 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.design.Dependency;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.MeasuresFilter;
+import org.sonar.api.measures.MeasuresFilters;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.batch.DefaultProjectTree;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.batch.sensor.DefaultSensorStorage;
+import org.sonar.core.component.ComponentKeys;
+
+public class DefaultIndex extends SonarIndex {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultIndex.class);
+
+ private final BatchComponentCache componentCache;
+ private final MeasureCache measureCache;
+ private final PathResolver pathResolver;
+ private final DefaultProjectTree projectTree;
+ // caches
+ private DefaultSensorStorage sensorStorage;
+ private Project currentProject;
+ private Map<Resource, Bucket> buckets = Maps.newLinkedHashMap();
+
+ public DefaultIndex(BatchComponentCache componentCache, DefaultProjectTree projectTree, MeasureCache measureCache, PathResolver pathResolver) {
+ this.componentCache = componentCache;
+ this.projectTree = projectTree;
+ this.measureCache = measureCache;
+ this.pathResolver = pathResolver;
+ }
+
+ public void start() {
+ Project rootProject = projectTree.getRootProject();
+ if (StringUtils.isNotBlank(rootProject.getKey())) {
+ doStart(rootProject);
+ }
+ }
+
+ void doStart(Project rootProject) {
+ Bucket bucket = new Bucket(rootProject);
+ addBucket(rootProject, bucket);
+ BatchComponent component = componentCache.add(rootProject, null);
+ component.setInputComponent(new DefaultInputModule(rootProject.getEffectiveKey()));
+ currentProject = rootProject;
+
+ for (Project module : rootProject.getModules()) {
+ addModule(rootProject, module);
+ }
+ }
+
+ private void addBucket(Resource resource, Bucket bucket) {
+ buckets.put(resource, bucket);
+ }
+
+ private void addModule(Project parent, Project module) {
+ ProjectDefinition parentDefinition = projectTree.getProjectDefinition(parent);
+ java.io.File parentBaseDir = parentDefinition.getBaseDir();
+ ProjectDefinition moduleDefinition = projectTree.getProjectDefinition(module);
+ java.io.File moduleBaseDir = moduleDefinition.getBaseDir();
+ module.setPath(new PathResolver().relativePath(parentBaseDir, moduleBaseDir));
+ addResource(module);
+ for (Project submodule : module.getModules()) {
+ addModule(module, submodule);
+ }
+ }
+
+ @Override
+ public Project getProject() {
+ return currentProject;
+ }
+
+ public void setCurrentProject(Project project, DefaultSensorStorage sensorStorage) {
+ this.currentProject = project;
+
+ // the following components depend on the current module, so they need to be reloaded.
+ this.sensorStorage = sensorStorage;
+ }
+
+ /**
+ * Keep only project stuff
+ */
+ public void clear() {
+ Iterator<Map.Entry<Resource, Bucket>> it = buckets.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<Resource, Bucket> entry = it.next();
+ Resource resource = entry.getKey();
+ if (!ResourceUtils.isSet(resource)) {
+ entry.getValue().clear();
+ it.remove();
+ }
+
+ }
+ }
+
+ @CheckForNull
+ @Override
+ public Measure getMeasure(Resource resource, org.sonar.api.batch.measure.Metric<?> metric) {
+ return getMeasures(resource, MeasuresFilters.metric(metric));
+ }
+
+ @CheckForNull
+ @Override
+ public <M> M getMeasures(Resource resource, MeasuresFilter<M> filter) {
+ // Reload resource so that effective key is populated
+ Resource indexedResource = getResource(resource);
+ if (indexedResource == null) {
+ return null;
+ }
+ Collection<Measure> unfiltered = new ArrayList<>();
+ if (filter instanceof MeasuresFilters.MetricFilter) {
+ // optimization
+ Measure byMetric = measureCache.byMetric(indexedResource, ((MeasuresFilters.MetricFilter<M>) filter).filterOnMetricKey());
+ if (byMetric != null) {
+ unfiltered.add(byMetric);
+ }
+ } else {
+ for (Measure measure : measureCache.byResource(indexedResource)) {
+ unfiltered.add(measure);
+ }
+ }
+ return filter.filter(unfiltered);
+ }
+
+ @Override
+ public Measure addMeasure(Resource resource, Measure measure) {
+ Bucket bucket = getBucket(resource);
+ if (bucket != null) {
+ return sensorStorage.saveMeasure(resource, measure);
+ }
+ return measure;
+ }
+
+ @Override
+ public Dependency addDependency(Dependency dependency) {
+ return dependency;
+ }
+
+ @Override
+ public Set<Resource> getResources() {
+ return buckets.keySet();
+ }
+
+ @Override
+ public String getSource(Resource reference) {
+ Resource resource = getResource(reference);
+ if (resource instanceof File) {
+ File file = (File) resource;
+ Project module = currentProject;
+ ProjectDefinition def = projectTree.getProjectDefinition(module);
+ try {
+ return FileUtils.readFileToString(new java.io.File(def.getBaseDir(), file.getPath()));
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to read file content " + reference, e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Does nothing if the resource is already registered.
+ */
+ @Override
+ public Resource addResource(Resource resource) {
+ Bucket bucket = doIndex(resource);
+ return bucket != null ? bucket.getResource() : null;
+ }
+
+ @Override
+ @CheckForNull
+ public <R extends Resource> R getResource(@Nullable R reference) {
+ Bucket bucket = getBucket(reference);
+ if (bucket != null) {
+ return (R) bucket.getResource();
+ }
+ return null;
+ }
+
+ @Override
+ public List<Resource> getChildren(Resource resource) {
+ List<Resource> children = Lists.newLinkedList();
+ Bucket bucket = getBucket(resource);
+ if (bucket != null) {
+ for (Bucket childBucket : bucket.getChildren()) {
+ children.add(childBucket.getResource());
+ }
+ }
+ return children;
+ }
+
+ @Override
+ public Resource getParent(Resource resource) {
+ Bucket bucket = getBucket(resource);
+ if (bucket != null && bucket.getParent() != null) {
+ return bucket.getParent().getResource();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean index(Resource resource) {
+ Bucket bucket = doIndex(resource);
+ return bucket != null;
+ }
+
+ private Bucket doIndex(Resource resource) {
+ if (resource.getParent() != null) {
+ doIndex(resource.getParent());
+ }
+ return doIndex(resource, resource.getParent());
+ }
+
+ @Override
+ public boolean index(Resource resource, Resource parentReference) {
+ Bucket bucket = doIndex(resource, parentReference);
+ return bucket != null;
+ }
+
+ private Bucket doIndex(Resource resource, @Nullable Resource parentReference) {
+ Bucket bucket = getBucket(resource);
+ if (bucket != null) {
+ return bucket;
+ }
+
+ if (StringUtils.isBlank(resource.getKey())) {
+ LOG.warn("Unable to index a resource without key " + resource);
+ return null;
+ }
+
+ Resource parent = (Resource) ObjectUtils.defaultIfNull(parentReference, currentProject);
+
+ Bucket parentBucket = getBucket(parent);
+ if (parentBucket == null && parent != null) {
+ LOG.warn("Resource ignored, parent is not indexed: " + resource);
+ return null;
+ }
+
+ if (ResourceUtils.isProject(resource) || /* For technical projects */ResourceUtils.isRootProject(resource)) {
+ resource.setEffectiveKey(resource.getKey());
+ } else {
+ resource.setEffectiveKey(ComponentKeys.createEffectiveKey(currentProject, resource));
+ }
+ bucket = new Bucket(resource).setParent(parentBucket);
+ addBucket(resource, bucket);
+
+ Resource parentResource = parentBucket != null ? parentBucket.getResource() : null;
+ BatchComponent component = componentCache.add(resource, parentResource);
+ if (ResourceUtils.isProject(resource)) {
+ component.setInputComponent(new DefaultInputModule(resource.getEffectiveKey()));
+ }
+
+ return bucket;
+ }
+
+ @Override
+ public boolean isExcluded(@Nullable Resource reference) {
+ return false;
+ }
+
+ @Override
+ public boolean isIndexed(@Nullable Resource reference, boolean acceptExcluded) {
+ return getBucket(reference) != null;
+ }
+
+ private Bucket getBucket(@Nullable Resource reference) {
+ if (reference == null) {
+ return null;
+ }
+ if (StringUtils.isNotBlank(reference.getKey())) {
+ return buckets.get(reference);
+ }
+ String relativePathFromSourceDir = null;
+ boolean isTest = false;
+ boolean isDir = false;
+ if (reference instanceof File) {
+ File referenceFile = (File) reference;
+ isTest = Qualifiers.UNIT_TEST_FILE.equals(referenceFile.getQualifier());
+ relativePathFromSourceDir = referenceFile.relativePathFromSourceDir();
+ } else if (reference instanceof Directory) {
+ isDir = true;
+ Directory referenceDir = (Directory) reference;
+ relativePathFromSourceDir = referenceDir.relativePathFromSourceDir();
+ if (Directory.ROOT.equals(relativePathFromSourceDir)) {
+ relativePathFromSourceDir = "";
+ }
+ }
+ if (relativePathFromSourceDir != null) {
+ // Resolve using deprecated key
+ List<String> dirs;
+ ProjectDefinition projectDef = projectTree.getProjectDefinition(getProject());
+ if (isTest) {
+ dirs = projectDef.getTestDirs();
+ } else {
+ dirs = projectDef.getSourceDirs();
+ }
+ for (String src : dirs) {
+ java.io.File dirOrFile = pathResolver.relativeFile(projectDef.getBaseDir(), src);
+ java.io.File abs = new java.io.File(dirOrFile, relativePathFromSourceDir);
+ Bucket b = getBucket(isDir ? Directory.fromIOFile(abs, getProject()) : File.fromIOFile(abs, getProject()));
+ if (b != null) {
+ return b;
+ }
+ }
+
+ }
+ return null;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/index/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/package-info.java
new file mode 100644
index 00000000000..6fc80086043
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/index/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.index;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultFilterableIssue.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultFilterableIssue.java
new file mode 100644
index 00000000000..12dac2c1902
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultFilterableIssue.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.util.Date;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.sonar.api.resources.Project;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+import org.sonar.scanner.protocol.output.ScannerReport.Issue;
+
+public class DefaultFilterableIssue implements FilterableIssue {
+ private final Issue rawIssue;
+ private final Project project;
+ private final String componentKey;
+
+ public DefaultFilterableIssue(Project project, Issue rawIssue, String componentKey) {
+ this.project = project;
+ this.rawIssue = rawIssue;
+ this.componentKey = componentKey;
+
+ }
+
+ @Override
+ public String componentKey() {
+ return componentKey;
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return RuleKey.of(rawIssue.getRuleRepository(), rawIssue.getRuleKey());
+ }
+
+ @Override
+ public String severity() {
+ return rawIssue.getSeverity().name();
+ }
+
+ @Override
+ public String message() {
+ return rawIssue.getMsg();
+ }
+
+ @Override
+ public Integer line() {
+ return rawIssue.hasLine() ? rawIssue.getLine() : null;
+ }
+
+ @Override
+ public Double gap() {
+ return rawIssue.hasGap() ? rawIssue.getGap() : null;
+ }
+
+ @Override
+ public Double effortToFix() {
+ return gap();
+ }
+
+ @Override
+ public Date creationDate() {
+ return project.getAnalysisDate();
+ }
+
+ @Override
+ public String projectKey() {
+ return project.getEffectiveKey();
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssuable.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssuable.java
new file mode 100644
index 00000000000..291fb7bb2ee
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssuable.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.util.Collections;
+import java.util.List;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
+import org.sonar.api.issue.Issuable;
+import org.sonar.api.issue.Issue;
+import org.sonar.batch.index.BatchComponent;
+
+/**
+ * @since 3.6
+ */
+public class DefaultIssuable implements Issuable {
+
+ private final BatchComponent component;
+ private final SensorContext sensorContext;
+
+ DefaultIssuable(BatchComponent component, SensorContext sensorContext) {
+ this.component = component;
+ this.sensorContext = sensorContext;
+ }
+
+ @Override
+ public IssueBuilder newIssueBuilder() {
+ DefaultIssue newIssue = (DefaultIssue) sensorContext.newIssue();
+ return new DeprecatedIssueBuilderWrapper(component.inputComponent(), newIssue);
+ }
+
+ @Override
+ public boolean addIssue(Issue issue) {
+ ((DeprecatedIssueWrapper) issue).wrapped().save();
+ return true;
+ }
+
+ @Override
+ public List<Issue> resolvedIssues() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<Issue> issues() {
+ return Collections.emptyList();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueCallback.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueCallback.java
new file mode 100644
index 00000000000..4c6a3b5c1e4
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueCallback.java
@@ -0,0 +1,121 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.batch.rule.Rules;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.scanner.protocol.input.ScannerInput.User;
+import org.sonar.batch.bootstrapper.IssueListener;
+
+public class DefaultIssueCallback implements IssueCallback {
+ private final IssueCache issues;
+ private final IssueListener listener;
+ private final UserRepositoryLoader userRepository;
+ private final Rules rules;
+
+ private Set<String> userLoginNames = new HashSet<>();
+ private Map<String, String> userMap = new HashMap<>();
+ private Set<RuleKey> ruleKeys = new HashSet<>();
+
+ public DefaultIssueCallback(IssueCache issues, IssueListener listener, UserRepositoryLoader userRepository, Rules rules) {
+ this.issues = issues;
+ this.listener = listener;
+ this.userRepository = userRepository;
+ this.rules = rules;
+ }
+
+ /**
+ * If no listener exists, this constructor will be used by pico.
+ */
+ public DefaultIssueCallback(IssueCache issues, UserRepositoryLoader userRepository, Rules rules) {
+ this(issues, null, userRepository, rules);
+ }
+
+ @Override
+ public void execute() {
+ if (listener == null) {
+ return;
+ }
+
+ for (TrackedIssue issue : issues.all()) {
+ collectInfo(issue);
+ }
+
+ getUsers();
+
+ for (TrackedIssue issue : issues.all()) {
+ IssueListener.Issue newIssue = new IssueListener.Issue();
+ newIssue.setAssigneeLogin(issue.assignee());
+ newIssue.setAssigneeName(getAssigneeName(issue.assignee()));
+ newIssue.setComponentKey(issue.componentKey());
+ newIssue.setKey(issue.key());
+ newIssue.setMessage(issue.getMessage());
+ newIssue.setNew(issue.isNew());
+ newIssue.setResolution(issue.resolution());
+ newIssue.setRuleKey(issue.getRuleKey().toString());
+ newIssue.setRuleName(getRuleName(issue.getRuleKey()));
+ newIssue.setSeverity(issue.severity());
+ newIssue.setStatus(issue.status());
+ newIssue.setStartLine(issue.startLine());
+ newIssue.setStartLineOffset(issue.startLineOffset());
+ newIssue.setEndLine(issue.endLine());
+ newIssue.setEndLineOffset(issue.endLineOffset());
+
+ listener.handle(newIssue);
+ }
+ }
+
+ private void collectInfo(TrackedIssue issue) {
+ if (!StringUtils.isEmpty(issue.assignee())) {
+ userLoginNames.add(issue.assignee());
+ }
+ if (issue.getRuleKey() != null) {
+ ruleKeys.add(issue.getRuleKey());
+ }
+ }
+
+ private String getAssigneeName(String login) {
+ return userMap.get(login);
+ }
+
+ private void getUsers() {
+ for (String loginName : userLoginNames) {
+ User user = userRepository.load(loginName);
+ if (user != null) {
+ userMap.put(user.getLogin(), user.getName());
+ }
+ }
+ }
+
+ private String getRuleName(RuleKey ruleKey) {
+ Rule rule = rules.find(ruleKey);
+ return rule != null ? rule.name() : null;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueFilterChain.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueFilterChain.java
new file mode 100644
index 00000000000..2d9b86ad4a8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultIssueFilterChain.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.scan.issue.filter.IssueFilter;
+
+import java.util.List;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+import org.sonar.api.scan.issue.filter.IssueFilterChain;
+
+public class DefaultIssueFilterChain implements IssueFilterChain {
+ private final List<IssueFilter> filters;
+
+ public DefaultIssueFilterChain(IssueFilter... filters) {
+ this.filters = ImmutableList.copyOf(filters);
+ }
+
+ public DefaultIssueFilterChain() {
+ this.filters = ImmutableList.of();
+ }
+
+ private DefaultIssueFilterChain(List<IssueFilter> filters) {
+ this.filters = filters;
+ }
+
+ @Override
+ public boolean accept(FilterableIssue issue) {
+ if (filters.isEmpty()) {
+ return true;
+ } else {
+ return filters.get(0).accept(issue, new DefaultIssueFilterChain(filters.subList(1, filters.size())));
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultProjectIssues.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultProjectIssues.java
new file mode 100644
index 00000000000..be81a1a2a53
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DefaultProjectIssues.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.ProjectIssues;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import javax.annotation.Nullable;
+
+/**
+ * Expose list of issues for the current project
+ * @since 4.0
+ */
+public class DefaultProjectIssues implements ProjectIssues {
+
+ private final IssueCache cache;
+
+ public DefaultProjectIssues(IssueCache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public Iterable<Issue> issues() {
+ return Iterables.transform(
+ Iterables.filter(cache.all(), new ResolvedPredicate(false)),
+ new IssueTransformer());
+ }
+
+ @Override
+ public Iterable<Issue> resolvedIssues() {
+ return Iterables.transform(
+ Iterables.filter(cache.all(), new ResolvedPredicate(true)),
+ new IssueTransformer());
+ }
+
+ private static class ResolvedPredicate implements Predicate<TrackedIssue> {
+ private final boolean resolved;
+
+ private ResolvedPredicate(boolean resolved) {
+ this.resolved = resolved;
+ }
+
+ @Override
+ public boolean apply(@Nullable TrackedIssue issue) {
+ if (issue != null) {
+ return resolved ? (issue.resolution() != null) : (issue.resolution() == null);
+ }
+ return false;
+ }
+ }
+
+ private static class IssueTransformer implements Function<TrackedIssue, Issue> {
+ @Override
+ public Issue apply(TrackedIssue issue) {
+ return new TrackedIssueAdapter(issue);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilter.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilter.java
new file mode 100644
index 00000000000..9dfa16d7f7f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilter.java
@@ -0,0 +1,193 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueComment;
+import org.sonar.api.resources.Project;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.Duration;
+
+/**
+ * @deprecated since 5.3
+ */
+@Deprecated
+class DeprecatedIssueAdapterForFilter implements Issue {
+ private final Project project;
+ private final org.sonar.scanner.protocol.output.ScannerReport.Issue rawIssue;
+ private final String componentKey;
+
+ DeprecatedIssueAdapterForFilter(Project project, org.sonar.scanner.protocol.output.ScannerReport.Issue rawIssue, String componentKey) {
+ this.project = project;
+ this.rawIssue = rawIssue;
+ this.componentKey = componentKey;
+ }
+
+ @Override
+ public String key() {
+ throw unsupported();
+ }
+
+ @Override
+ public String componentKey() {
+ return componentKey;
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return RuleKey.of(rawIssue.getRuleRepository(), rawIssue.getRuleKey());
+ }
+
+ @Override
+ public String language() {
+ throw unsupported();
+ }
+
+ @Override
+ public String severity() {
+ return rawIssue.getSeverity().name();
+ }
+
+ @Override
+ public String message() {
+ return rawIssue.getMsg();
+ }
+
+ @Override
+ public Integer line() {
+ return rawIssue.hasLine() ? rawIssue.getLine() : null;
+ }
+
+ @Override
+ @Deprecated
+ public Double effortToFix() {
+ return gap();
+ }
+
+ @Override
+ public Double gap() {
+ return rawIssue.hasGap() ? rawIssue.getGap() : null;
+ }
+
+ @Override
+ public String status() {
+ return Issue.STATUS_OPEN;
+ }
+
+ @Override
+ public String resolution() {
+ return null;
+ }
+
+ @Override
+ public String reporter() {
+ throw unsupported();
+ }
+
+ @Override
+ public String assignee() {
+ return null;
+ }
+
+ @Override
+ public Date creationDate() {
+ return project.getAnalysisDate();
+ }
+
+ @Override
+ public Date updateDate() {
+ return null;
+ }
+
+ @Override
+ public Date closeDate() {
+ return null;
+ }
+
+ @Override
+ public String attribute(String key) {
+ return attributes().get(key);
+ }
+
+ @Override
+ public Map<String, String> attributes() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String authorLogin() {
+ throw unsupported();
+ }
+
+ @Override
+ public String actionPlanKey() {
+ throw unsupported();
+ }
+
+ @Override
+ public List<IssueComment> comments() {
+ throw unsupported();
+ }
+
+ @Override
+ public boolean isNew() {
+ throw unsupported();
+ }
+
+ @Deprecated
+ @Override
+ public Duration debt() {
+ return effort();
+ }
+
+ @Override
+ public Duration effort() {
+ throw unsupported();
+ }
+
+ @Override
+ public String projectKey() {
+ return project.getEffectiveKey();
+ }
+
+ @Override
+ public String projectUuid() {
+ throw unsupported();
+ }
+
+ @Override
+ public String componentUuid() {
+ throw unsupported();
+ }
+
+ @Override
+ public Collection<String> tags() {
+ throw unsupported();
+ }
+
+ private static UnsupportedOperationException unsupported() {
+ return new UnsupportedOperationException("Not available for issues filters");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueBuilderWrapper.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueBuilderWrapper.java
new file mode 100644
index 00000000000..0ebef7b6c73
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueBuilderWrapper.java
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
+import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation;
+import org.sonar.api.issue.Issuable;
+import org.sonar.api.issue.Issuable.IssueBuilder;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rule.RuleKey;
+
+public class DeprecatedIssueBuilderWrapper implements Issuable.IssueBuilder {
+
+ private final DefaultIssue newIssue;
+ private final InputComponent primaryComponent;
+ private TextRange primaryRange = null;
+ private String primaryMessage = null;
+
+ public DeprecatedIssueBuilderWrapper(InputComponent primaryComponent, DefaultIssue newIssue) {
+ this.primaryComponent = primaryComponent;
+ this.newIssue = newIssue;
+ }
+
+ @Override
+ public IssueBuilder ruleKey(RuleKey ruleKey) {
+ newIssue.forRule(ruleKey);
+ return this;
+ }
+
+ @Override
+ public IssueBuilder line(@Nullable Integer line) {
+ Preconditions.checkState(newIssue.primaryLocation() == null, "Do not use line() and at() for the same issue");
+ if (primaryComponent.isFile()) {
+ if (line != null) {
+ this.primaryRange = ((InputFile) primaryComponent).selectLine(line.intValue());
+ }
+ return this;
+ } else {
+ throw new IllegalArgumentException("Unable to set line for issues on project or directory");
+ }
+ }
+
+ @Override
+ public IssueBuilder message(String message) {
+ Preconditions.checkState(newIssue.primaryLocation() == null, "Do not use message() and at() for the same issue");
+ this.primaryMessage = message;
+ return this;
+ }
+
+ @Override
+ public NewIssueLocation newLocation() {
+ return new DefaultIssueLocation();
+ }
+
+ @Override
+ public IssueBuilder at(NewIssueLocation primaryLocation) {
+ Preconditions.checkState(primaryMessage == null && primaryRange == null, "Do not use message() or line() and at() for the same issue");
+ newIssue.at(primaryLocation);
+ return this;
+ }
+
+ @Override
+ public IssueBuilder addLocation(NewIssueLocation secondaryLocation) {
+ newIssue.addLocation(secondaryLocation);
+ return this;
+ }
+
+ @Override
+ public IssueBuilder addFlow(Iterable<NewIssueLocation> flowLocations) {
+ newIssue.addFlow(flowLocations);
+ return this;
+ }
+
+ @Override
+ public IssueBuilder severity(String severity) {
+ newIssue.overrideSeverity(Severity.valueOf(severity));
+ return this;
+ }
+
+ @Override
+ public IssueBuilder reporter(String reporter) {
+ throw new UnsupportedOperationException("Not supported during sensor phase");
+ }
+
+ @Override
+ public IssueBuilder effortToFix(Double d) {
+ newIssue.effortToFix(d);
+ return this;
+ }
+
+ @Override
+ public IssueBuilder attribute(String key, String value) {
+ throw new UnsupportedOperationException("Not supported during sensor phase");
+ }
+
+ @Override
+ public Issue build() {
+ if (newIssue.primaryLocation() == null) {
+ NewIssueLocation newLocation = newIssue.newLocation().on(primaryComponent);
+ if (primaryMessage != null) {
+ newLocation.message(primaryMessage);
+ }
+ if (primaryComponent.isFile() && primaryRange != null) {
+ newLocation.at(primaryRange);
+ }
+ newIssue.at(newLocation);
+ }
+ return new DeprecatedIssueWrapper(newIssue);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueFilterChain.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueFilterChain.java
new file mode 100644
index 00000000000..83994cfb49d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueFilterChain.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.batch.IssueFilter;
+import org.sonar.api.issue.batch.IssueFilterChain;
+
+import java.util.List;
+
+/**
+ * @deprecated since 5.3
+ */
+@Deprecated
+public class DeprecatedIssueFilterChain implements IssueFilterChain {
+
+ private final List<IssueFilter> filters;
+
+ public DeprecatedIssueFilterChain(IssueFilter... filters) {
+ this.filters = ImmutableList.copyOf(filters);
+ }
+
+ public DeprecatedIssueFilterChain() {
+ this.filters = ImmutableList.of();
+ }
+
+ private DeprecatedIssueFilterChain(List<IssueFilter> filters) {
+ this.filters = filters;
+ }
+
+ @Override
+ public boolean accept(Issue issue) {
+ if (filters.isEmpty()) {
+ return true;
+ } else {
+ return filters.get(0).accept(issue, new DeprecatedIssueFilterChain(filters.subList(1, filters.size())));
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueWrapper.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueWrapper.java
new file mode 100644
index 00000000000..61e215cc931
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/DeprecatedIssueWrapper.java
@@ -0,0 +1,193 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueComment;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.Duration;
+
+public class DeprecatedIssueWrapper implements Issue {
+
+ private final DefaultIssue newIssue;
+
+ public DeprecatedIssueWrapper(DefaultIssue newIssue) {
+ this.newIssue = newIssue;
+ }
+
+ public DefaultIssue wrapped() {
+ return newIssue;
+ }
+
+ @Override
+ public String key() {
+ return null;
+ }
+
+ @Override
+ public String componentKey() {
+ return null;
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return newIssue.ruleKey();
+ }
+
+ @Override
+ public String language() {
+ return null;
+ }
+
+ @Override
+ public String severity() {
+ Severity overridenSeverity = newIssue.overriddenSeverity();
+ return overridenSeverity != null ? overridenSeverity.name() : null;
+ }
+
+ @Override
+ public String message() {
+ return newIssue.primaryLocation().message();
+ }
+
+ @Override
+ public Integer line() {
+ TextRange textRange = newIssue.primaryLocation().textRange();
+ return textRange != null ? textRange.start().line() : null;
+ }
+
+ /**
+ * @deprecated since 5.5, replaced by {@link #gap()}
+ */
+ @Override
+ @Deprecated
+ public Double effortToFix() {
+ return gap();
+ }
+
+ @Override
+ public Double gap() {
+ return newIssue.effortToFix();
+ }
+
+ @Override
+ public String status() {
+ return null;
+ }
+
+ @Override
+ public String resolution() {
+ return null;
+ }
+
+ @Override
+ public String reporter() {
+ return null;
+ }
+
+ @Override
+ public String assignee() {
+ return null;
+ }
+
+ @Override
+ public Date creationDate() {
+ return null;
+ }
+
+ @Override
+ public Date updateDate() {
+ return null;
+ }
+
+ @Override
+ public Date closeDate() {
+ return null;
+ }
+
+ @Override
+ public String attribute(String key) {
+ return null;
+ }
+
+ @Override
+ public Map<String, String> attributes() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String authorLogin() {
+ return null;
+ }
+
+ @Override
+ public String actionPlanKey() {
+ return null;
+ }
+
+ @Override
+ public List<IssueComment> comments() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isNew() {
+ return false;
+ }
+
+ @Override
+ public Duration debt() {
+ return null;
+ }
+
+ @Override
+ public Duration effort() {
+ return null;
+ }
+
+ @Override
+ public String projectKey() {
+ return null;
+ }
+
+ @Override
+ public String projectUuid() {
+ return null;
+ }
+
+ @Override
+ public String componentUuid() {
+ return null;
+ }
+
+ @Override
+ public Collection<String> tags() {
+ return Collections.emptyList();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssuableFactory.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssuableFactory.java
new file mode 100644
index 00000000000..a2c7afc9c59
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssuableFactory.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.issue.Issuable;
+import org.sonar.batch.deprecated.perspectives.PerspectiveBuilder;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.sensor.DefaultSensorContext;
+
+/**
+ * Create the perspective {@link Issuable} on components.
+ * @since 3.6
+ */
+public class IssuableFactory extends PerspectiveBuilder<Issuable> {
+
+ private final SensorContext sensorContext;
+
+ public IssuableFactory(DefaultSensorContext sensorContext) {
+ super(Issuable.class);
+ this.sensorContext = sensorContext;
+ }
+
+ @Override
+ public Issuable loadPerspective(Class<Issuable> perspectiveClass, BatchComponent component) {
+ return new DefaultIssuable(component, sensorContext);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCache.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCache.java
new file mode 100644
index 00000000000..ac1bc6826a5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCache.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.batch.index.Cache;
+import org.sonar.batch.index.Caches;
+
+import java.util.Collection;
+
+/**
+ * Shared issues among all project modules
+ */
+@BatchSide
+public class IssueCache {
+
+ // component key -> issue key -> issue
+ private final Cache<TrackedIssue> cache;
+
+ public IssueCache(Caches caches) {
+ cache = caches.createCache("issues");
+ }
+
+ public Iterable<TrackedIssue> byComponent(String componentKey) {
+ return cache.values(componentKey);
+ }
+
+ public Iterable<TrackedIssue> all() {
+ return cache.values();
+ }
+
+ public Collection<Object> componentKeys() {
+ return cache.keySet();
+ }
+
+ public IssueCache put(TrackedIssue issue) {
+ cache.put(issue.componentKey(), issue.key(), issue);
+ return this;
+ }
+
+ public void clear(String componentKey) {
+ cache.clear(componentKey);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCallback.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCallback.java
new file mode 100644
index 00000000000..b27b7887045
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueCallback.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+public interface IssueCallback {
+ void execute();
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueFilters.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueFilters.java
new file mode 100644
index 00000000000..ef542d551ab
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueFilters.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import org.sonar.api.scan.issue.filter.IssueFilterChain;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.scan.issue.filter.IssueFilter;
+import org.sonar.api.resources.Project;
+
+@BatchSide
+public class IssueFilters {
+ private final IssueFilter[] filters;
+ private final org.sonar.api.issue.batch.IssueFilter[] deprecatedFilters;
+ private final Project project;
+
+ public IssueFilters(Project project, IssueFilter[] exclusionFilters, org.sonar.api.issue.batch.IssueFilter[] filters) {
+ this.project = project;
+ this.filters = exclusionFilters;
+ this.deprecatedFilters = filters;
+ }
+
+ public IssueFilters(Project project, IssueFilter[] filters) {
+ this(project, filters, new org.sonar.api.issue.batch.IssueFilter[0]);
+ }
+
+ public IssueFilters(Project project, org.sonar.api.issue.batch.IssueFilter[] deprecatedFilters) {
+ this(project, new IssueFilter[0], deprecatedFilters);
+ }
+
+ public IssueFilters(Project project) {
+ this(project, new IssueFilter[0], new org.sonar.api.issue.batch.IssueFilter[0]);
+ }
+
+ public boolean accept(String componentKey, ScannerReport.Issue rawIssue) {
+ IssueFilterChain filterChain = new DefaultIssueFilterChain(filters);
+ FilterableIssue fIssue = new DefaultFilterableIssue(project, rawIssue, componentKey);
+ if (filterChain.accept(fIssue)) {
+ return acceptDeprecated(componentKey, rawIssue);
+ }
+
+ return false;
+ }
+
+ public boolean acceptDeprecated(String componentKey, ScannerReport.Issue rawIssue) {
+ Issue issue = new DeprecatedIssueAdapterForFilter(project, rawIssue, componentKey);
+ return new DeprecatedIssueFilterChain(deprecatedFilters).accept(issue);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueTransformer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueTransformer.java
new file mode 100644
index 00000000000..8a34253114b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/IssueTransformer.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.issue.tracking.SourceHashHolder;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import org.sonar.core.component.ComponentKeys;
+import org.sonar.core.util.Uuids;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReport.TextRange;
+
+public class IssueTransformer {
+ private IssueTransformer() {
+ // static only
+ }
+
+ public static TrackedIssue toTrackedIssue(ServerIssue serverIssue) {
+ TrackedIssue issue = new TrackedIssue();
+ issue.setKey(serverIssue.getKey());
+ issue.setStatus(serverIssue.getStatus());
+ issue.setResolution(serverIssue.hasResolution() ? serverIssue.getResolution() : null);
+ issue.setMessage(serverIssue.hasMsg() ? serverIssue.getMsg() : null);
+ issue.setStartLine(serverIssue.hasLine() ? serverIssue.getLine() : null);
+ issue.setEndLine(serverIssue.hasLine() ? serverIssue.getLine() : null);
+ issue.setSeverity(serverIssue.getSeverity().name());
+ issue.setAssignee(serverIssue.hasAssigneeLogin() ? serverIssue.getAssigneeLogin() : null);
+ issue.setComponentKey(ComponentKeys.createEffectiveKey(serverIssue.getModuleKey(), serverIssue.hasPath() ? serverIssue.getPath() : null));
+ issue.setCreationDate(new Date(serverIssue.getCreationDate()));
+ issue.setRuleKey(RuleKey.of(serverIssue.getRuleRepository(), serverIssue.getRuleKey()));
+ issue.setNew(false);
+ return issue;
+ }
+
+ public static void close(TrackedIssue issue) {
+ issue.setStatus(Issue.STATUS_CLOSED);
+ issue.setStartLine(null);
+ issue.setEndLine(null);
+ issue.setResolution(Issue.RESOLUTION_FIXED);
+ }
+
+ public static void resolveRemove(TrackedIssue issue) {
+ issue.setStatus(Issue.STATUS_CLOSED);
+ issue.setStartLine(null);
+ issue.setEndLine(null);
+ issue.setResolution(Issue.RESOLUTION_REMOVED);
+ }
+
+ public static Collection<TrackedIssue> toTrackedIssue(BatchComponent component, Collection<ScannerReport.Issue> rawIssues, @Nullable SourceHashHolder hashes) {
+ List<TrackedIssue> issues = new ArrayList<>(rawIssues.size());
+
+ for (ScannerReport.Issue issue : rawIssues) {
+ issues.add(toTrackedIssue(component, issue, hashes));
+ }
+
+ return issues;
+ }
+
+ public static TrackedIssue toTrackedIssue(BatchComponent component, ScannerReport.Issue rawIssue, @Nullable SourceHashHolder hashes) {
+ RuleKey ruleKey = RuleKey.of(rawIssue.getRuleRepository(), rawIssue.getRuleKey());
+
+ Preconditions.checkNotNull(component.key(), "Component key must be set");
+ Preconditions.checkNotNull(ruleKey, "Rule key must be set");
+
+ TrackedIssue issue = new TrackedIssue(hashes != null ? hashes.getHashedSource() : null);
+
+ issue.setKey(Uuids.createFast());
+ issue.setComponentKey(component.key());
+ issue.setRuleKey(ruleKey);
+ issue.setGap(rawIssue.hasGap() ? rawIssue.getGap() : null);
+ issue.setSeverity(rawIssue.getSeverity().name());
+ issue.setMessage(rawIssue.hasMsg() ? rawIssue.getMsg() : null);
+ issue.setResolution(null);
+ issue.setStatus(Issue.STATUS_OPEN);
+ issue.setNew(true);
+
+ if (rawIssue.hasTextRange()) {
+ TextRange r = rawIssue.getTextRange();
+
+ issue.setStartLine(r.hasStartLine() ? rawIssue.getTextRange().getStartLine() : null);
+ issue.setStartLineOffset(r.hasStartOffset() ? r.getStartOffset() : null);
+ issue.setEndLine(r.hasEndLine() ? r.getEndLine() : issue.startLine());
+ issue.setEndLineOffset(r.hasEndOffset() ? r.getEndOffset() : null);
+ }
+
+ return issue;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ModuleIssues.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ModuleIssues.java
new file mode 100644
index 00000000000..433c8826f85
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ModuleIssues.java
@@ -0,0 +1,155 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.base.Strings;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.rule.ActiveRule;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.batch.rule.Rules;
+import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.issue.Issue.Flow;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.scanner.protocol.Constants.Severity;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReport.IssueLocation;
+import org.sonar.scanner.protocol.output.ScannerReport.IssueLocation.Builder;
+
+/**
+ * Initialize the issues raised during scan.
+ */
+public class ModuleIssues {
+
+ private final ActiveRules activeRules;
+ private final Rules rules;
+ private final IssueFilters filters;
+ private final ReportPublisher reportPublisher;
+ private final BatchComponentCache componentCache;
+ private final ScannerReport.Issue.Builder builder = ScannerReport.Issue.newBuilder();
+ private final Builder locationBuilder = IssueLocation.newBuilder();
+ private final org.sonar.scanner.protocol.output.ScannerReport.TextRange.Builder textRangeBuilder = org.sonar.scanner.protocol.output.ScannerReport.TextRange.newBuilder();
+ private final ScannerReport.Flow.Builder flowBuilder = ScannerReport.Flow.newBuilder();
+
+ public ModuleIssues(ActiveRules activeRules, Rules rules, IssueFilters filters, ReportPublisher reportPublisher, BatchComponentCache componentCache) {
+ this.activeRules = activeRules;
+ this.rules = rules;
+ this.filters = filters;
+ this.reportPublisher = reportPublisher;
+ this.componentCache = componentCache;
+ }
+
+ public boolean initAndAddIssue(Issue issue) {
+ InputComponent inputComponent = issue.primaryLocation().inputComponent();
+ BatchComponent component = componentCache.get(inputComponent);
+
+ Rule rule = validateRule(issue);
+ ActiveRule activeRule = activeRules.find(issue.ruleKey());
+ if (activeRule == null) {
+ // rule does not exist or is not enabled -> ignore the issue
+ return false;
+ }
+
+ String primaryMessage = Strings.isNullOrEmpty(issue.primaryLocation().message()) ? rule.name() : issue.primaryLocation().message();
+ org.sonar.api.batch.rule.Severity overriddenSeverity = issue.overriddenSeverity();
+ Severity severity = overriddenSeverity != null ? Severity.valueOf(overriddenSeverity.name()) : Severity.valueOf(activeRule.severity());
+
+ builder.clear();
+ locationBuilder.clear();
+ // non-null fields
+ builder.setSeverity(severity);
+ builder.setRuleRepository(issue.ruleKey().repository());
+ builder.setRuleKey(issue.ruleKey().rule());
+ builder.setMsg(primaryMessage);
+ locationBuilder.setMsg(primaryMessage);
+
+ locationBuilder.setComponentRef(component.batchId());
+ TextRange primaryTextRange = issue.primaryLocation().textRange();
+ if (primaryTextRange != null) {
+ builder.setLine(primaryTextRange.start().line());
+ builder.setTextRange(toProtobufTextRange(primaryTextRange));
+ }
+ Double gap = issue.gap();
+ if (gap != null) {
+ builder.setGap(gap);
+ }
+ applyFlows(issue);
+ ScannerReport.Issue rawIssue = builder.build();
+
+ if (filters.accept(inputComponent.key(), rawIssue)) {
+ write(component, rawIssue);
+ return true;
+ }
+ return false;
+ }
+
+ private void applyFlows(Issue issue) {
+ for (Flow flow : issue.flows()) {
+ if (!flow.locations().isEmpty()) {
+ flowBuilder.clear();
+ for (org.sonar.api.batch.sensor.issue.IssueLocation location : flow.locations()) {
+ locationBuilder.clear();
+ locationBuilder.setComponentRef(componentCache.get(location.inputComponent()).batchId());
+ String message = location.message();
+ if (message != null) {
+ locationBuilder.setMsg(message);
+ }
+ TextRange textRange = location.textRange();
+ if (textRange != null) {
+ locationBuilder.setTextRange(toProtobufTextRange(textRange));
+ }
+ flowBuilder.addLocation(locationBuilder.build());
+ }
+ builder.addFlow(flowBuilder.build());
+ }
+ }
+ }
+
+ private org.sonar.scanner.protocol.output.ScannerReport.TextRange toProtobufTextRange(TextRange primaryTextRange) {
+ textRangeBuilder.clear();
+ textRangeBuilder.setStartLine(primaryTextRange.start().line());
+ textRangeBuilder.setStartOffset(primaryTextRange.start().lineOffset());
+ textRangeBuilder.setEndLine(primaryTextRange.end().line());
+ textRangeBuilder.setEndOffset(primaryTextRange.end().lineOffset());
+ return textRangeBuilder.build();
+ }
+
+ private Rule validateRule(Issue issue) {
+ RuleKey ruleKey = issue.ruleKey();
+ Rule rule = rules.find(ruleKey);
+ if (rule == null) {
+ throw MessageException.of(String.format("The rule '%s' does not exist.", ruleKey));
+ }
+ if (Strings.isNullOrEmpty(rule.name()) && Strings.isNullOrEmpty(issue.primaryLocation().message())) {
+ throw MessageException.of(String.format("The rule '%s' has no name and the related issue has no message.", ruleKey));
+ }
+ return rule;
+ }
+
+ public void write(BatchComponent component, ScannerReport.Issue rawIssue) {
+ reportPublisher.getWriter().appendComponentIssue(component.batchId(), rawIssue);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/TrackedIssueAdapter.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/TrackedIssueAdapter.java
new file mode 100644
index 00000000000..19027847d9d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/TrackedIssueAdapter.java
@@ -0,0 +1,202 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueComment;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.Duration;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+public class TrackedIssueAdapter implements Issue {
+ private TrackedIssue issue;
+
+ public TrackedIssueAdapter(TrackedIssue issue) {
+ this.issue = issue;
+ }
+
+ @Override
+ public String key() {
+ return issue.key();
+ }
+
+ @Override
+ public String componentKey() {
+ return issue.componentKey();
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return issue.getRuleKey();
+ }
+
+ @Override
+ public String severity() {
+ return issue.severity();
+ }
+
+ @Override
+ public String message() {
+ return issue.getMessage();
+ }
+
+ @Override
+ public Integer line() {
+ return issue.startLine();
+ }
+
+ /**
+ * @deprecated since 5.5, replaced by {@link #gap()}
+ */
+ @Override
+ @Deprecated
+ public Double effortToFix() {
+ return gap();
+ }
+
+ @Override
+ public Double gap() {
+ return issue.gap();
+ }
+
+ @Override
+ public String status() {
+ return issue.status();
+ }
+
+ @Override
+ public String resolution() {
+ return issue.resolution();
+ }
+
+ @Override
+ public String reporter() {
+ return issue.reporter();
+ }
+
+ @Override
+ public String assignee() {
+ return issue.assignee();
+ }
+
+ @Override
+ public boolean isNew() {
+ return issue.isNew();
+ }
+
+ @Override
+ public Map<String, String> attributes() {
+ return new HashMap<>();
+ }
+
+ @Override
+ public Date creationDate() {
+ return issue.getCreationDate();
+ }
+
+ @Override
+ public String language() {
+ return null;
+ }
+
+ @Override
+ public Date updateDate() {
+ return null;
+ }
+
+ @Override
+ public Date closeDate() {
+ return null;
+ }
+
+ @Override
+ public String attribute(String key) {
+ return attributes().get(key);
+ }
+
+ @Override
+ public String authorLogin() {
+ return null;
+ }
+
+ @Override
+ public String actionPlanKey() {
+ return null;
+ }
+
+ @Override
+ public List<IssueComment> comments() {
+ return new ArrayList<>();
+ }
+
+ /**
+ * @deprecated since 5.5, replaced by {@link #effort()}
+ */
+ @Override
+ @Deprecated
+ public Duration debt() {
+ return null;
+ }
+
+ @Override
+ public Duration effort() {
+ return null;
+ }
+
+ @Override
+ public String projectKey() {
+ return null;
+ }
+
+ @Override
+ public String projectUuid() {
+ return null;
+ }
+
+ @Override
+ public String componentUuid() {
+ return null;
+ }
+
+ @Override
+ public Collection<String> tags() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || !(o instanceof Issue)) {
+ return false;
+ }
+ Issue that = (Issue) o;
+ return !(issue.key() != null ? !issue.key().equals(that.key()) : (that.key() != null));
+ }
+
+ @Override
+ public int hashCode() {
+ return issue.key() != null ? issue.key().hashCode() : 0;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/EnforceIssuesFilter.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/EnforceIssuesFilter.java
new file mode 100644
index 00000000000..de4920a1f06
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/EnforceIssuesFilter.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import org.sonar.batch.issue.ignore.pattern.IssueInclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssuePattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.scan.issue.filter.IssueFilter;
+import org.sonar.api.scan.issue.filter.IssueFilterChain;
+
+public class EnforceIssuesFilter implements IssueFilter {
+
+ private IssueInclusionPatternInitializer patternInitializer;
+
+ private static final Logger LOG = LoggerFactory.getLogger(EnforceIssuesFilter.class);
+
+ public EnforceIssuesFilter(IssueInclusionPatternInitializer patternInitializer) {
+ this.patternInitializer = patternInitializer;
+ }
+
+ @Override
+ public boolean accept(FilterableIssue issue, IssueFilterChain chain) {
+ boolean atLeastOneRuleMatched = false;
+ boolean atLeastOnePatternFullyMatched = false;
+ IssuePattern matchingPattern = null;
+
+ for (IssuePattern pattern : patternInitializer.getMulticriteriaPatterns()) {
+ if (pattern.getRulePattern().match(issue.ruleKey().toString())) {
+ atLeastOneRuleMatched = true;
+ String pathForComponent = patternInitializer.getPathForComponent(issue.componentKey());
+ if (pathForComponent != null && pattern.getResourcePattern().match(pathForComponent)) {
+ atLeastOnePatternFullyMatched = true;
+ matchingPattern = pattern;
+ }
+ }
+ }
+
+ if (atLeastOneRuleMatched) {
+ if (atLeastOnePatternFullyMatched) {
+ LOG.debug("Issue {} enforced by pattern {}", issue, matchingPattern);
+ }
+ return atLeastOnePatternFullyMatched;
+ } else {
+ return chain.accept(issue);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilter.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilter.java
new file mode 100644
index 00000000000..99a716148c6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilter.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssuePattern;
+import org.sonar.batch.issue.ignore.pattern.PatternMatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.scan.issue.filter.IssueFilter;
+import org.sonar.api.scan.issue.filter.IssueFilterChain;
+
+public class IgnoreIssuesFilter implements IssueFilter {
+
+ private PatternMatcher patternMatcher;
+
+ private static final Logger LOG = LoggerFactory.getLogger(IgnoreIssuesFilter.class);
+
+ public IgnoreIssuesFilter(IssueExclusionPatternInitializer patternInitializer) {
+ this.patternMatcher = patternInitializer.getPatternMatcher();
+ }
+
+ @Override
+ public boolean accept(FilterableIssue issue, IssueFilterChain chain) {
+ if (hasMatchFor(issue)) {
+ return false;
+ } else {
+ return chain.accept(issue);
+ }
+ }
+
+ private boolean hasMatchFor(FilterableIssue issue) {
+ IssuePattern pattern = patternMatcher.getMatchingPattern(issue);
+ if (pattern != null) {
+ logExclusion(issue, pattern);
+ return true;
+ }
+ return false;
+ }
+
+ private static void logExclusion(FilterableIssue issue, IssuePattern pattern) {
+ LOG.debug("Issue {} ignored by exclusion pattern {}", issue, pattern);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/package-info.java
new file mode 100644
index 00000000000..ead2a62c636
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.issue.ignore;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/AbstractPatternInitializer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/AbstractPatternInitializer.java
new file mode 100644
index 00000000000..a3e58803a71
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/AbstractPatternInitializer.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.config.Settings;
+
+import java.util.List;
+
+import static com.google.common.base.Objects.firstNonNull;
+
+@BatchSide
+public abstract class AbstractPatternInitializer {
+
+ private Settings settings;
+
+ private List<IssuePattern> multicriteriaPatterns;
+
+ protected AbstractPatternInitializer(Settings settings) {
+ this.settings = settings;
+ initPatterns();
+ }
+
+ protected Settings getSettings() {
+ return settings;
+ }
+
+ public List<IssuePattern> getMulticriteriaPatterns() {
+ return multicriteriaPatterns;
+ }
+
+ public boolean hasConfiguredPatterns() {
+ return hasMulticriteriaPatterns();
+ }
+
+ public boolean hasMulticriteriaPatterns() {
+ return !multicriteriaPatterns.isEmpty();
+ }
+
+ public abstract void initializePatternsForPath(String relativePath, String componentKey);
+
+ @VisibleForTesting
+ protected final void initPatterns() {
+ // Patterns Multicriteria
+ multicriteriaPatterns = Lists.newArrayList();
+ String patternConf = StringUtils.defaultIfBlank(settings.getString(getMulticriteriaConfigurationKey()), "");
+ for (String id : StringUtils.split(patternConf, ',')) {
+ String propPrefix = getMulticriteriaConfigurationKey() + "." + id + ".";
+ String resourceKeyPattern = settings.getString(propPrefix + "resourceKey");
+ String ruleKeyPattern = settings.getString(propPrefix + "ruleKey");
+ String lineRange = "*";
+ String[] fields = new String[] {resourceKeyPattern, ruleKeyPattern, lineRange};
+ PatternDecoder.checkRegularLineConstraints(StringUtils.join(fields, ","), fields);
+ IssuePattern pattern = new IssuePattern(firstNonNull(resourceKeyPattern, "*"), firstNonNull(ruleKeyPattern, "*"));
+ PatternDecoder.decodeRangeOfLines(pattern, firstNonNull(lineRange, "*"));
+ multicriteriaPatterns.add(pattern);
+ }
+ }
+
+ protected abstract String getMulticriteriaConfigurationKey();
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializer.java
new file mode 100644
index 00000000000..74d80f95186
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializer.java
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.config.Settings;
+import org.sonar.core.config.IssueExclusionProperties;
+
+import java.util.List;
+
+import static com.google.common.base.Strings.nullToEmpty;
+
+public class IssueExclusionPatternInitializer extends AbstractPatternInitializer {
+
+ private List<IssuePattern> blockPatterns;
+ private List<IssuePattern> allFilePatterns;
+ private PatternMatcher patternMatcher;
+
+ public IssueExclusionPatternInitializer(Settings settings) {
+ super(settings);
+ patternMatcher = new PatternMatcher();
+ loadFileContentPatterns();
+ }
+
+ @Override
+ protected String getMulticriteriaConfigurationKey() {
+ return "sonar.issue.ignore" + ".multicriteria";
+ }
+
+ public PatternMatcher getPatternMatcher() {
+ return patternMatcher;
+ }
+
+ @Override
+ public void initializePatternsForPath(String relativePath, String componentKey) {
+ for (IssuePattern pattern : getMulticriteriaPatterns()) {
+ if (pattern.matchResource(relativePath)) {
+ getPatternMatcher().addPatternForComponent(componentKey, pattern);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasConfiguredPatterns() {
+ return hasFileContentPattern() || hasMulticriteriaPatterns();
+ }
+
+ @VisibleForTesting
+ protected final void loadFileContentPatterns() {
+ // Patterns Block
+ blockPatterns = Lists.newArrayList();
+ String patternConf = StringUtils.defaultIfBlank(getSettings().getString(IssueExclusionProperties.PATTERNS_BLOCK_KEY), "");
+ for (String id : StringUtils.split(patternConf, ',')) {
+ String propPrefix = IssueExclusionProperties.PATTERNS_BLOCK_KEY + "." + id + ".";
+ String beginBlockRegexp = getSettings().getString(propPrefix + IssueExclusionProperties.BEGIN_BLOCK_REGEXP);
+ String endBlockRegexp = getSettings().getString(propPrefix + IssueExclusionProperties.END_BLOCK_REGEXP);
+ String[] fields = new String[]{beginBlockRegexp, endBlockRegexp};
+ PatternDecoder.checkDoubleRegexpLineConstraints(StringUtils.join(fields, ","), fields);
+ IssuePattern pattern = new IssuePattern().setBeginBlockRegexp(nullToEmpty(beginBlockRegexp)).setEndBlockRegexp(nullToEmpty(endBlockRegexp));
+ blockPatterns.add(pattern);
+ }
+
+ // Patterns All File
+ allFilePatterns = Lists.newArrayList();
+ patternConf = StringUtils.defaultIfBlank(getSettings().getString(IssueExclusionProperties.PATTERNS_ALLFILE_KEY), "");
+ for (String id : StringUtils.split(patternConf, ',')) {
+ String propPrefix = IssueExclusionProperties.PATTERNS_ALLFILE_KEY + "." + id + ".";
+ String allFileRegexp = getSettings().getString(propPrefix + IssueExclusionProperties.FILE_REGEXP);
+ PatternDecoder.checkWholeFileRegexp(allFileRegexp);
+ IssuePattern pattern = new IssuePattern().setAllFileRegexp(nullToEmpty(allFileRegexp));
+ allFilePatterns.add(pattern);
+ }
+ }
+
+ public List<IssuePattern> getBlockPatterns() {
+ return blockPatterns;
+ }
+
+ public List<IssuePattern> getAllFilePatterns() {
+ return allFilePatterns;
+ }
+
+ public boolean hasFileContentPattern() {
+ return !(blockPatterns.isEmpty() && allFilePatterns.isEmpty());
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializer.java
new file mode 100644
index 00000000000..9fec33bb952
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializer.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import com.google.common.collect.Maps;
+import org.sonar.api.config.Settings;
+
+import java.util.Map;
+
+public class IssueInclusionPatternInitializer extends AbstractPatternInitializer {
+
+ private Map<String, String> pathForComponent;
+
+ public IssueInclusionPatternInitializer(Settings settings) {
+ super(settings);
+ pathForComponent = Maps.newHashMap();
+ }
+
+ @Override
+ protected String getMulticriteriaConfigurationKey() {
+ return "sonar.issue.enforce" + ".multicriteria";
+ }
+
+ @Override
+ public void initializePatternsForPath(String relativePath, String componentKey) {
+ pathForComponent.put(componentKey, relativePath);
+ }
+
+ public String getPathForComponent(String componentKey) {
+ return pathForComponent.get(componentKey);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssuePattern.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssuePattern.java
new file mode 100644
index 00000000000..4c4cc0b4715
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/IssuePattern.java
@@ -0,0 +1,167 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import com.google.common.collect.Sets;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.WildcardPattern;
+
+import java.util.Set;
+
+public class IssuePattern {
+
+ private WildcardPattern resourcePattern;
+ private WildcardPattern rulePattern;
+ private Set<Integer> lines = Sets.newLinkedHashSet();
+ private Set<LineRange> lineRanges = Sets.newLinkedHashSet();
+ private String beginBlockRegexp;
+ private String endBlockRegexp;
+ private String allFileRegexp;
+ private boolean checkLines = true;
+
+ public IssuePattern() {
+ }
+
+ public IssuePattern(String resourcePattern, String rulePattern) {
+ this.resourcePattern = WildcardPattern.create(resourcePattern);
+ this.rulePattern = WildcardPattern.create(rulePattern);
+ }
+
+ public IssuePattern(String resourcePattern, String rulePattern, Set<LineRange> lineRanges) {
+ this(resourcePattern, rulePattern);
+ this.lineRanges = lineRanges;
+ }
+
+ public WildcardPattern getResourcePattern() {
+ return resourcePattern;
+ }
+
+ public WildcardPattern getRulePattern() {
+ return rulePattern;
+ }
+
+ public String getBeginBlockRegexp() {
+ return beginBlockRegexp;
+ }
+
+ public String getEndBlockRegexp() {
+ return endBlockRegexp;
+ }
+
+ public String getAllFileRegexp() {
+ return allFileRegexp;
+ }
+
+ IssuePattern addLineRange(int fromLineId, int toLineId) {
+ lineRanges.add(new LineRange(fromLineId, toLineId));
+ return this;
+ }
+
+ IssuePattern addLine(int lineId) {
+ lines.add(lineId);
+ return this;
+ }
+
+ boolean isCheckLines() {
+ return checkLines;
+ }
+
+ IssuePattern setCheckLines(boolean b) {
+ this.checkLines = b;
+ return this;
+ }
+
+ IssuePattern setBeginBlockRegexp(String beginBlockRegexp) {
+ this.beginBlockRegexp = beginBlockRegexp;
+ return this;
+ }
+
+ IssuePattern setEndBlockRegexp(String endBlockRegexp) {
+ this.endBlockRegexp = endBlockRegexp;
+ return this;
+ }
+
+ IssuePattern setAllFileRegexp(String allFileRegexp) {
+ this.allFileRegexp = allFileRegexp;
+ return this;
+ }
+
+ Set<Integer> getAllLines() {
+ Set<Integer> allLines = Sets.newLinkedHashSet(lines);
+ for (LineRange lineRange : lineRanges) {
+ allLines.addAll(lineRange.toLines());
+ }
+ return allLines;
+ }
+
+ public boolean match(FilterableIssue issue) {
+ boolean match = matchResource(issue.componentKey())
+ && matchRule(issue.ruleKey());
+ if (checkLines) {
+ Integer line = issue.line();
+ if (line == null) {
+ match = false;
+ } else {
+ match = match && matchLine(line);
+ }
+ }
+ return match;
+ }
+
+ boolean matchLine(int lineId) {
+ if (lines.contains(lineId)) {
+ return true;
+ }
+
+ for (LineRange range : lineRanges) {
+ if (range.in(lineId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ boolean matchRule(RuleKey rule) {
+ if (rule == null) {
+ return false;
+ }
+
+ String key = new StringBuilder().append(rule.repository()).append(':').append(rule.rule()).toString();
+ return rulePattern.match(key);
+ }
+
+ boolean matchResource(String resource) {
+ return resource != null && resourcePattern.match(resource);
+ }
+
+ public IssuePattern forResource(String resource) {
+ return new IssuePattern(resource, rulePattern.toString(), lineRanges).setCheckLines(isCheckLines());
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/LineRange.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/LineRange.java
new file mode 100644
index 00000000000..72d43f5f216
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/LineRange.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+public class LineRange {
+ private int from;
+ private int to;
+
+ public LineRange(int from, int to) {
+ Preconditions.checkArgument(from <= to, "Line range is not valid: %s must be greater than %s", from, to);
+
+ this.from = from;
+ this.to = to;
+ }
+
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ return lines;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + from + "-" + to + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + from;
+ result = prime * result + to;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ if (fieldsDiffer((LineRange) obj)) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean fieldsDiffer(LineRange other) {
+ if (from != other.from) {
+ return true;
+ }
+ if (to != other.to) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternDecoder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternDecoder.java
new file mode 100644
index 00000000000..11dc501cac4
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternDecoder.java
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.SonarException;
+
+import java.util.List;
+
+public class PatternDecoder {
+
+ private static final int THREE_FIELDS_PER_LINE = 3;
+ private static final String LINE_RANGE_REGEXP = "\\[((\\d+|\\d+-\\d+),?)*\\]";
+ private static final String CONFIG_FORMAT_ERROR_PREFIX = "Exclusions > Issues : Invalid format. ";
+
+ public List<IssuePattern> decode(String patternsList) {
+ List<IssuePattern> patterns = Lists.newLinkedList();
+ String[] patternsLines = StringUtils.split(patternsList, "\n");
+ for (String patternLine : patternsLines) {
+ IssuePattern pattern = decodeLine(patternLine.trim());
+ if (pattern != null) {
+ patterns.add(pattern);
+ }
+ }
+ return patterns;
+ }
+
+ /**
+ * Main method that decodes a line which defines a pattern
+ */
+ public IssuePattern decodeLine(String line) {
+ if (isBlankOrComment(line)) {
+ return null;
+ }
+
+ String[] fields = StringUtils.splitPreserveAllTokens(line, ';');
+ if (fields.length > THREE_FIELDS_PER_LINE) {
+ throw new SonarException(CONFIG_FORMAT_ERROR_PREFIX + "The following line has more than 3 fields separated by comma: " + line);
+ }
+
+ IssuePattern pattern;
+ if (fields.length == THREE_FIELDS_PER_LINE) {
+ checkRegularLineConstraints(line, fields);
+ pattern = new IssuePattern(StringUtils.trim(fields[0]), StringUtils.trim(fields[1]));
+ decodeRangeOfLines(pattern, fields[2]);
+ } else if (fields.length == 2) {
+ checkDoubleRegexpLineConstraints(line, fields);
+ pattern = new IssuePattern().setBeginBlockRegexp(fields[0]).setEndBlockRegexp(fields[1]);
+ } else {
+ checkWholeFileRegexp(fields[0]);
+ pattern = new IssuePattern().setAllFileRegexp(fields[0]);
+ }
+
+ return pattern;
+ }
+
+ static void checkRegularLineConstraints(String line, String[] fields) {
+ if (!isResource(fields[0])) {
+ throw new SonarException(CONFIG_FORMAT_ERROR_PREFIX + "The first field does not define a resource pattern: " + line);
+ }
+ if (!isRule(fields[1])) {
+ throw new SonarException(CONFIG_FORMAT_ERROR_PREFIX + "The second field does not define a rule pattern: " + line);
+ }
+ if (!isLinesRange(fields[2])) {
+ throw new SonarException(CONFIG_FORMAT_ERROR_PREFIX + "The third field does not define a range of lines: " + line);
+ }
+ }
+
+ static void checkDoubleRegexpLineConstraints(String line, String[] fields) {
+ if (!isRegexp(fields[0])) {
+ throw new SonarException(CONFIG_FORMAT_ERROR_PREFIX + "The first field does not define a regular expression: " + line);
+ }
+ // As per configuration help, missing second field means: from start regexp to EOF
+ }
+
+ static void checkWholeFileRegexp(String regexp) {
+ if (!isRegexp(regexp)) {
+ throw new SonarException(CONFIG_FORMAT_ERROR_PREFIX + "The field does not define a regular expression: " + regexp);
+ }
+ }
+
+ public static void decodeRangeOfLines(IssuePattern pattern, String field) {
+ if (StringUtils.equals(field, "*")) {
+ pattern.setCheckLines(false);
+ } else {
+ pattern.setCheckLines(true);
+ String s = StringUtils.substringBetween(StringUtils.trim(field), "[", "]");
+ String[] parts = StringUtils.split(s, ',');
+ for (String part : parts) {
+ if (StringUtils.contains(part, '-')) {
+ String[] range = StringUtils.split(part, '-');
+ pattern.addLineRange(Integer.valueOf(range[0]), Integer.valueOf(range[1]));
+ } else {
+ pattern.addLine(Integer.valueOf(part));
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static boolean isLinesRange(String field) {
+ return StringUtils.equals(field, "*") || java.util.regex.Pattern.matches(LINE_RANGE_REGEXP, field);
+ }
+
+ @VisibleForTesting
+ static boolean isBlankOrComment(String line) {
+ return StringUtils.isBlank(line) ^ StringUtils.startsWith(line, "#");
+ }
+
+ @VisibleForTesting
+ static boolean isResource(String field) {
+ return StringUtils.isNotBlank(field);
+ }
+
+ @VisibleForTesting
+ static boolean isRule(String field) {
+ return StringUtils.isNotBlank(field);
+ }
+
+ @VisibleForTesting
+ static boolean isRegexp(String field) {
+ return StringUtils.isNotBlank(field);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternMatcher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternMatcher.java
new file mode 100644
index 00000000000..80b6f716cff
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/PatternMatcher.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+
+public class PatternMatcher {
+
+ private Multimap<String, IssuePattern> patternByComponent = LinkedHashMultimap.create();
+
+ public IssuePattern getMatchingPattern(FilterableIssue issue) {
+ IssuePattern matchingPattern = null;
+ Iterator<IssuePattern> patternIterator = getPatternsForComponent(issue.componentKey()).iterator();
+ while(matchingPattern == null && patternIterator.hasNext()) {
+ IssuePattern nextPattern = patternIterator.next();
+ if (nextPattern.match(issue)) {
+ matchingPattern = nextPattern;
+ }
+ }
+ return matchingPattern;
+ }
+
+ public Collection<IssuePattern> getPatternsForComponent(String componentKey) {
+ return patternByComponent.get(componentKey);
+ }
+
+ public void addPatternForComponent(String component, IssuePattern pattern) {
+ patternByComponent.put(component, pattern.forResource(component));
+ }
+
+ public void addPatternToExcludeResource(String resource) {
+ addPatternForComponent(resource, new IssuePattern(resource, "*").setCheckLines(false));
+ }
+
+ public void addPatternToExcludeLines(String resource, Set<LineRange> lineRanges) {
+ addPatternForComponent(resource, new IssuePattern(resource, "*", lineRanges).setCheckLines(true));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/package-info.java
new file mode 100644
index 00000000000..ebbc7b8371e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/pattern/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.issue.ignore.pattern;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoader.java
new file mode 100644
index 00000000000..8622dc618eb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoader.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.scanner;
+
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssueInclusionPatternInitializer;
+
+import java.nio.charset.Charset;
+
+public final class IssueExclusionsLoader {
+
+ private final IssueExclusionsRegexpScanner regexpScanner;
+ private final IssueExclusionPatternInitializer exclusionPatternInitializer;
+ private final IssueInclusionPatternInitializer inclusionPatternInitializer;
+ private final FileSystem fileSystem;
+
+ public IssueExclusionsLoader(IssueExclusionsRegexpScanner regexpScanner, IssueExclusionPatternInitializer exclusionPatternInitializer,
+ IssueInclusionPatternInitializer inclusionPatternInitializer,
+ FileSystem fileSystem) {
+ this.regexpScanner = regexpScanner;
+ this.exclusionPatternInitializer = exclusionPatternInitializer;
+ this.inclusionPatternInitializer = inclusionPatternInitializer;
+ this.fileSystem = fileSystem;
+ }
+
+ public boolean shouldExecuteOnProject(Project project) {
+ return inclusionPatternInitializer.hasConfiguredPatterns()
+ || exclusionPatternInitializer.hasConfiguredPatterns();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void execute() {
+ Charset sourcesEncoding = fileSystem.encoding();
+
+ for (InputFile inputFile : fileSystem.inputFiles(fileSystem.predicates().all())) {
+ try {
+ String componentEffectiveKey = ((DefaultInputFile) inputFile).key();
+ if (componentEffectiveKey != null) {
+ String path = inputFile.relativePath();
+ inclusionPatternInitializer.initializePatternsForPath(path, componentEffectiveKey);
+ exclusionPatternInitializer.initializePatternsForPath(path, componentEffectiveKey);
+ if (exclusionPatternInitializer.hasFileContentPattern()) {
+ regexpScanner.scan(componentEffectiveKey, inputFile.file(), sourcesEncoding);
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to read the source file : '" + inputFile.absolutePath() + "' with the charset : '"
+ + sourcesEncoding.name() + "'.", e);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Issues Exclusions - Source Scanner";
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScanner.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScanner.java
new file mode 100644
index 00000000000..de465ad4dc5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScanner.java
@@ -0,0 +1,198 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.scanner;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssuePattern;
+import org.sonar.batch.issue.ignore.pattern.LineRange;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Set;
+
+@BatchSide
+public class IssueExclusionsRegexpScanner {
+
+ private static final Logger LOG = LoggerFactory.getLogger(IssueExclusionsRegexpScanner.class);
+
+ private IssueExclusionPatternInitializer exclusionPatternInitializer;
+ private List<java.util.regex.Pattern> allFilePatterns;
+ private List<DoubleRegexpMatcher> blockMatchers;
+
+ // fields to be reset at every new scan
+ private DoubleRegexpMatcher currentMatcher;
+ private int fileLength;
+ private List<LineExclusion> lineExclusions;
+ private LineExclusion currentLineExclusion;
+
+ public IssueExclusionsRegexpScanner(IssueExclusionPatternInitializer patternsInitializer) {
+ this.exclusionPatternInitializer = patternsInitializer;
+
+ lineExclusions = Lists.newArrayList();
+ allFilePatterns = Lists.newArrayList();
+ blockMatchers = Lists.newArrayList();
+
+ for (IssuePattern pattern : patternsInitializer.getAllFilePatterns()) {
+ allFilePatterns.add(java.util.regex.Pattern.compile(pattern.getAllFileRegexp()));
+ }
+ for (IssuePattern pattern : patternsInitializer.getBlockPatterns()) {
+ blockMatchers.add(new DoubleRegexpMatcher(
+ java.util.regex.Pattern.compile(pattern.getBeginBlockRegexp()),
+ java.util.regex.Pattern.compile(pattern.getEndBlockRegexp())));
+ }
+
+ init();
+ }
+
+ private void init() {
+ currentMatcher = null;
+ fileLength = 0;
+ lineExclusions.clear();
+ currentLineExclusion = null;
+ }
+
+ public void scan(String resource, File file, Charset sourcesEncoding) throws IOException {
+ LOG.debug("Scanning {}", resource);
+ init();
+
+ List<String> lines = FileUtils.readLines(file, sourcesEncoding.name());
+ int lineIndex = 0;
+ for (String line : lines) {
+ lineIndex++;
+ if (line.trim().length() == 0) {
+ continue;
+ }
+
+ // first check the single regexp patterns that can be used to totally exclude a file
+ for (java.util.regex.Pattern pattern : allFilePatterns) {
+ if (pattern.matcher(line).find()) {
+ exclusionPatternInitializer.getPatternMatcher().addPatternToExcludeResource(resource);
+ // nothing more to do on this file
+ LOG.debug("- Exclusion pattern '{}': every violation in this file will be ignored.", pattern);
+ return;
+ }
+ }
+
+ // then check the double regexps if we're still here
+ checkDoubleRegexps(line, lineIndex);
+ }
+
+ if (currentMatcher != null && !currentMatcher.hasSecondPattern()) {
+ // this will happen when there is a start block regexp but no end block regexp
+ endExclusion(lineIndex + 1);
+ }
+
+ // now create the new line-based pattern for this file if there are exclusions
+ fileLength = lineIndex;
+ if (!lineExclusions.isEmpty()) {
+ Set<LineRange> lineRanges = convertLineExclusionsToLineRanges();
+ LOG.debug("- Line exclusions found: {}", lineRanges);
+ exclusionPatternInitializer.getPatternMatcher().addPatternToExcludeLines(resource, lineRanges);
+ }
+ }
+
+ private Set<LineRange> convertLineExclusionsToLineRanges() {
+ Set<LineRange> lineRanges = Sets.newHashSet();
+ for (LineExclusion lineExclusion : lineExclusions) {
+ lineRanges.add(lineExclusion.toLineRange());
+ }
+ return lineRanges;
+ }
+
+ private void checkDoubleRegexps(String line, int lineIndex) {
+ if (currentMatcher == null) {
+ for (DoubleRegexpMatcher matcher : blockMatchers) {
+ if (matcher.matchesFirstPattern(line)) {
+ startExclusion(lineIndex);
+ currentMatcher = matcher;
+ break;
+ }
+ }
+ } else {
+ if (currentMatcher.matchesSecondPattern(line)) {
+ endExclusion(lineIndex);
+ currentMatcher = null;
+ }
+ }
+ }
+
+ private void startExclusion(int lineIndex) {
+ currentLineExclusion = new LineExclusion(lineIndex);
+ lineExclusions.add(currentLineExclusion);
+ }
+
+ private void endExclusion(int lineIndex) {
+ currentLineExclusion.setEnd(lineIndex);
+ currentLineExclusion = null;
+ }
+
+ private class LineExclusion {
+
+ private int start;
+ private int end;
+
+ LineExclusion(int start) {
+ this.start = start;
+ this.end = -1;
+ }
+
+ void setEnd(int end) {
+ this.end = end;
+ }
+
+ public LineRange toLineRange() {
+ return new LineRange(start, end == -1 ? fileLength : end);
+ }
+
+ }
+
+ private static class DoubleRegexpMatcher {
+
+ private java.util.regex.Pattern firstPattern;
+ private java.util.regex.Pattern secondPattern;
+
+ DoubleRegexpMatcher(java.util.regex.Pattern firstPattern, java.util.regex.Pattern secondPattern) {
+ this.firstPattern = firstPattern;
+ this.secondPattern = secondPattern;
+ }
+
+ boolean matchesFirstPattern(String line) {
+ return firstPattern.matcher(line).find();
+ }
+
+ boolean matchesSecondPattern(String line) {
+ return hasSecondPattern() && secondPattern.matcher(line).find();
+ }
+
+ boolean hasSecondPattern() {
+ return StringUtils.isNotEmpty(secondPattern.toString());
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/package-info.java
new file mode 100644
index 00000000000..3beb574f061
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/ignore/scanner/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.issue.ignore.scanner;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/package-info.java
new file mode 100644
index 00000000000..75db5554d7f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.issue;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoader.java
new file mode 100644
index 00000000000..954997466fe
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoader.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+import javax.annotation.Nullable;
+
+import org.sonar.batch.util.BatchUtils;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterators;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+
+public class DefaultServerLineHashesLoader implements ServerLineHashesLoader {
+
+ private final WSLoader wsLoader;
+
+ public DefaultServerLineHashesLoader(WSLoader wsLoader) {
+ this.wsLoader = wsLoader;
+ }
+
+ @Override
+ public String[] getLineHashes(String fileKey, @Nullable MutableBoolean fromCache) {
+ String hashesFromWs = loadHashesFromWs(fileKey, fromCache);
+ return Iterators.toArray(Splitter.on('\n').split(hashesFromWs).iterator(), String.class);
+ }
+
+ private String loadHashesFromWs(String fileKey, @Nullable MutableBoolean fromCache) {
+ Profiler profiler = Profiler.createIfDebug(Loggers.get(getClass()))
+ .addContext("file", fileKey)
+ .startDebug("Load line hashes");
+ WSLoaderResult<String> result = wsLoader.loadString("/api/sources/hash?key=" + BatchUtils.encodeForUrl(fileKey), LoadStrategy.CACHE_FIRST);
+ try {
+ if (fromCache != null) {
+ fromCache.setValue(result.isFromCache());
+ }
+ return result.get();
+ } finally {
+ if (result.isFromCache()) {
+ profiler.stopDebug("Load line hashes (done from cache)");
+ } else {
+ profiler.stopDebug();
+ }
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java
new file mode 100644
index 00000000000..3284b5a681b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.batch.fs.internal.FileMetadata.LineHashConsumer;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang.ObjectUtils;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+
+import javax.annotation.Nullable;
+
+import java.util.Collection;
+
+/**
+ * Wraps a {@link Sequence} to assign hash codes to elements.
+ */
+public final class FileHashes {
+
+ private final String[] hashes;
+ private final Multimap<String, Integer> linesByHash;
+
+ private FileHashes(String[] hashes, Multimap<String, Integer> linesByHash) {
+ this.hashes = hashes;
+ this.linesByHash = linesByHash;
+ }
+
+ public static FileHashes create(String[] hashes) {
+ int size = hashes.length;
+ Multimap<String, Integer> linesByHash = LinkedHashMultimap.create();
+ for (int i = 0; i < size; i++) {
+ // indices in array are shifted one line before
+ linesByHash.put(hashes[i], i + 1);
+ }
+ return new FileHashes(hashes, linesByHash);
+ }
+
+ public static FileHashes create(DefaultInputFile f) {
+ final byte[][] hashes = new byte[f.lines()][];
+ FileMetadata.computeLineHashesForIssueTracking(f, new LineHashConsumer() {
+
+ @Override
+ public void consume(int lineIdx, @Nullable byte[] hash) {
+ hashes[lineIdx - 1] = hash;
+ }
+ });
+
+ int size = hashes.length;
+ Multimap<String, Integer> linesByHash = LinkedHashMultimap.create();
+ String[] hexHashes = new String[size];
+ for (int i = 0; i < size; i++) {
+ String hash = hashes[i] != null ? Hex.encodeHexString(hashes[i]) : "";
+ hexHashes[i] = hash;
+ // indices in array are shifted one line before
+ linesByHash.put(hash, i + 1);
+ }
+ return new FileHashes(hexHashes, linesByHash);
+ }
+
+ public int length() {
+ return hashes.length;
+ }
+
+ public Collection<Integer> getLinesForHash(String hash) {
+ return linesByHash.get(hash);
+ }
+
+ public String[] hashes() {
+ return hashes;
+ }
+
+ public String getHash(int line) {
+ // indices in array are shifted one line before
+ return (String) ObjectUtils.defaultIfNull(hashes[line - 1], "");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingInput.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingInput.java
new file mode 100644
index 00000000000..031396b54d7
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingInput.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.core.issue.tracking.Trackable;
+import org.sonar.core.issue.tracking.BlockHashSequence;
+import org.sonar.core.issue.tracking.LineHashSequence;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.sonar.core.issue.tracking.Input;
+
+public class IssueTrackingInput<T extends Trackable> implements Input<T> {
+
+ private final Collection<T> issues;
+ private final LineHashSequence lineHashes;
+ private final BlockHashSequence blockHashes;
+
+ public IssueTrackingInput(Collection<T> issues, List<String> hashes) {
+ this.issues = issues;
+ this.lineHashes = new LineHashSequence(hashes);
+ this.blockHashes = BlockHashSequence.create(lineHashes);
+ }
+
+ @Override
+ public LineHashSequence getLineHashSequence() {
+ return lineHashes;
+ }
+
+ @Override
+ public BlockHashSequence getBlockHashSequence() {
+ return blockHashes;
+ }
+
+ @Override
+ public Collection<T> getIssues() {
+ return issues;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTransition.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTransition.java
new file mode 100644
index 00000000000..bff1974a99d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/IssueTransition.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.batch.util.ProgressReport;
+import org.sonar.batch.issue.IssueTransformer;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@BatchSide
+public class IssueTransition {
+ private final IssueCache issueCache;
+ private final BatchComponentCache componentCache;
+ private final ReportPublisher reportPublisher;
+ private final Date analysisDate;
+ @Nullable
+ private final LocalIssueTracking localIssueTracking;
+
+ public IssueTransition(BatchComponentCache componentCache, IssueCache issueCache, ReportPublisher reportPublisher,
+ @Nullable LocalIssueTracking localIssueTracking) {
+ this.componentCache = componentCache;
+ this.issueCache = issueCache;
+ this.reportPublisher = reportPublisher;
+ this.localIssueTracking = localIssueTracking;
+ this.analysisDate = ((Project) componentCache.getRoot().resource()).getAnalysisDate();
+ }
+
+ public IssueTransition(BatchComponentCache componentCache, IssueCache issueCache, ReportPublisher reportPublisher) {
+ this(componentCache, issueCache, reportPublisher, null);
+ }
+
+ public void execute() {
+ if (localIssueTracking != null) {
+ localIssueTracking.init();
+ }
+
+ ScannerReportReader reader = new ScannerReportReader(reportPublisher.getReportDir());
+ int nbComponents = componentCache.all().size();
+
+ if (nbComponents == 0) {
+ return;
+ }
+
+ ProgressReport progressReport = new ProgressReport("issue-tracking-report", TimeUnit.SECONDS.toMillis(10));
+ progressReport.start("Performing issue tracking");
+ int count = 0;
+
+ try {
+ for (BatchComponent component : componentCache.all()) {
+ trackIssues(reader, component);
+ count++;
+ progressReport.message(count + "/" + nbComponents + " components tracked");
+ }
+ } finally {
+ progressReport.stop(count + "/" + nbComponents + " components tracked");
+ }
+ }
+
+ public void trackIssues(ScannerReportReader reader, BatchComponent component) {
+ // raw issues = all the issues created by rule engines during this module scan and not excluded by filters
+ List<ScannerReport.Issue> rawIssues = new LinkedList<>();
+ try (CloseableIterator<ScannerReport.Issue> it = reader.readComponentIssues(component.batchId())) {
+ while (it.hasNext()) {
+ rawIssues.add(it.next());
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Can't read issues for " + component.key(), e);
+ }
+
+ List<TrackedIssue> trackedIssues;
+ if (localIssueTracking != null) {
+ trackedIssues = localIssueTracking.trackIssues(component, rawIssues, analysisDate);
+ } else {
+ trackedIssues = doTransition(rawIssues, component);
+ }
+
+ for (TrackedIssue issue : trackedIssues) {
+ issueCache.put(issue);
+ }
+ }
+
+ private static List<TrackedIssue> doTransition(List<ScannerReport.Issue> rawIssues, BatchComponent component) {
+ List<TrackedIssue> issues = new ArrayList<>(rawIssues.size());
+
+ for (ScannerReport.Issue issue : rawIssues) {
+ issues.add(IssueTransformer.toTrackedIssue(component, issue, null));
+ }
+
+ return issues;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java
new file mode 100644
index 00000000000..634326771d4
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java
@@ -0,0 +1,266 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.core.issue.tracking.Tracking;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.core.issue.tracking.Input;
+import org.sonar.core.issue.tracking.Tracker;
+import org.sonar.batch.issue.IssueTransformer;
+import org.sonar.api.batch.fs.InputFile.Status;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.rule.ActiveRule;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.repository.ProjectRepositories;
+
+@BatchSide
+public class LocalIssueTracking {
+ private final Tracker<TrackedIssue, ServerIssueFromWs> tracker;
+ private final ServerLineHashesLoader lastLineHashes;
+ private final ActiveRules activeRules;
+ private final ServerIssueRepository serverIssueRepository;
+ private final DefaultAnalysisMode mode;
+
+ private boolean hasServerAnalysis;
+
+ public LocalIssueTracking(Tracker<TrackedIssue, ServerIssueFromWs> tracker, ServerLineHashesLoader lastLineHashes,
+ ActiveRules activeRules, ServerIssueRepository serverIssueRepository, ProjectRepositories projectRepositories, DefaultAnalysisMode mode) {
+ this.tracker = tracker;
+ this.lastLineHashes = lastLineHashes;
+ this.serverIssueRepository = serverIssueRepository;
+ this.mode = mode;
+ this.activeRules = activeRules;
+ this.hasServerAnalysis = projectRepositories.lastAnalysisDate() != null;
+ }
+
+ public void init() {
+ if (hasServerAnalysis) {
+ serverIssueRepository.load();
+ }
+ }
+
+ public List<TrackedIssue> trackIssues(BatchComponent component, Collection<ScannerReport.Issue> reportIssues, Date analysisDate) {
+ List<TrackedIssue> trackedIssues = new LinkedList<>();
+ if (hasServerAnalysis) {
+ // all the issues that are not closed in db before starting this module scan, including manual issues
+ Collection<ServerIssueFromWs> serverIssues = loadServerIssues(component);
+
+ if (shouldCopyServerIssues(component)) {
+ // raw issues should be empty, we just need to deal with server issues (SONAR-6931)
+ copyServerIssues(serverIssues, trackedIssues);
+ } else {
+
+ SourceHashHolder sourceHashHolder = loadSourceHashes(component);
+ Collection<TrackedIssue> rIssues = IssueTransformer.toTrackedIssue(component, reportIssues, sourceHashHolder);
+
+ Input<ServerIssueFromWs> baseIssues = createBaseInput(serverIssues, sourceHashHolder);
+ Input<TrackedIssue> rawIssues = createRawInput(rIssues, sourceHashHolder);
+
+ Tracking<TrackedIssue, ServerIssueFromWs> track = tracker.track(rawIssues, baseIssues);
+
+ addUnmatchedFromServer(track.getUnmatchedBases(), sourceHashHolder, trackedIssues);
+ addUnmatchedFromServer(track.getOpenManualIssuesByLine().values(), sourceHashHolder, trackedIssues);
+ mergeMatched(track, trackedIssues, rIssues);
+ addUnmatchedFromReport(track.getUnmatchedRaws(), trackedIssues, analysisDate);
+ }
+ }
+
+ if (hasServerAnalysis && ResourceUtils.isRootProject(component.resource())) {
+ // issues that relate to deleted components
+ addIssuesOnDeletedComponents(trackedIssues);
+ }
+
+ return trackedIssues;
+ }
+
+ private static Input<ServerIssueFromWs> createBaseInput(Collection<ServerIssueFromWs> serverIssues, @Nullable SourceHashHolder sourceHashHolder) {
+ List<String> refHashes;
+
+ if (sourceHashHolder != null && sourceHashHolder.getHashedReference() != null) {
+ refHashes = Arrays.asList(sourceHashHolder.getHashedReference().hashes());
+ } else {
+ refHashes = new ArrayList<>(0);
+ }
+
+ return new IssueTrackingInput<>(serverIssues, refHashes);
+ }
+
+ private static Input<TrackedIssue> createRawInput(Collection<TrackedIssue> rIssues, @Nullable SourceHashHolder sourceHashHolder) {
+ List<String> baseHashes;
+ if (sourceHashHolder != null && sourceHashHolder.getHashedSource() != null) {
+ baseHashes = Arrays.asList(sourceHashHolder.getHashedSource().hashes());
+ } else {
+ baseHashes = new ArrayList<>(0);
+ }
+
+ return new IssueTrackingInput<>(rIssues, baseHashes);
+ }
+
+ private boolean shouldCopyServerIssues(BatchComponent component) {
+ if (!mode.scanAllFiles() && component.isFile()) {
+ DefaultInputFile inputFile = (DefaultInputFile) component.inputComponent();
+ if (inputFile.status() == Status.SAME) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void copyServerIssues(Collection<ServerIssueFromWs> serverIssues, List<TrackedIssue> trackedIssues) {
+ for (ServerIssueFromWs serverIssue : serverIssues) {
+ org.sonar.scanner.protocol.input.ScannerInput.ServerIssue unmatchedPreviousIssue = serverIssue.getDto();
+ TrackedIssue unmatched = IssueTransformer.toTrackedIssue(unmatchedPreviousIssue);
+
+ ActiveRule activeRule = activeRules.find(unmatched.getRuleKey());
+ unmatched.setNew(false);
+
+ if (activeRule == null) {
+ // rule removed
+ IssueTransformer.resolveRemove(unmatched);
+ }
+
+ trackedIssues.add(unmatched);
+ }
+ }
+
+ @CheckForNull
+ private SourceHashHolder loadSourceHashes(BatchComponent component) {
+ SourceHashHolder sourceHashHolder = null;
+ if (component.isFile()) {
+ DefaultInputFile file = (DefaultInputFile) component.inputComponent();
+ if (file == null) {
+ throw new IllegalStateException("Resource " + component.resource() + " was not found in InputPath cache");
+ }
+ sourceHashHolder = new SourceHashHolder(file, lastLineHashes);
+ }
+ return sourceHashHolder;
+ }
+
+ private Collection<ServerIssueFromWs> loadServerIssues(BatchComponent component) {
+ Collection<ServerIssueFromWs> serverIssues = new ArrayList<>();
+ for (org.sonar.scanner.protocol.input.ScannerInput.ServerIssue previousIssue : serverIssueRepository.byComponent(component)) {
+ serverIssues.add(new ServerIssueFromWs(previousIssue));
+ }
+ return serverIssues;
+ }
+
+ @VisibleForTesting
+ protected void mergeMatched(Tracking<TrackedIssue, ServerIssueFromWs> result, Collection<TrackedIssue> mergeTo, Collection<TrackedIssue> rawIssues) {
+ for (Map.Entry<TrackedIssue, ServerIssueFromWs> e : result.getMatchedRaws().entrySet()) {
+ org.sonar.scanner.protocol.input.ScannerInput.ServerIssue dto = e.getValue().getDto();
+ TrackedIssue tracked = e.getKey();
+
+ // invariant fields
+ tracked.setKey(dto.getKey());
+
+ // non-persisted fields
+ tracked.setNew(false);
+
+ // fields to update with old values
+ tracked.setResolution(dto.hasResolution() ? dto.getResolution() : null);
+ tracked.setStatus(dto.getStatus());
+ tracked.setAssignee(dto.hasAssigneeLogin() ? dto.getAssigneeLogin() : null);
+ tracked.setCreationDate(new Date(dto.getCreationDate()));
+
+ if (dto.getManualSeverity()) {
+ // Severity overriden by user
+ tracked.setSeverity(dto.getSeverity().name());
+ }
+ mergeTo.add(tracked);
+ }
+ }
+
+ private void addUnmatchedFromServer(Iterable<ServerIssueFromWs> unmatchedIssues, SourceHashHolder sourceHashHolder, Collection<TrackedIssue> mergeTo) {
+ for (ServerIssueFromWs unmatchedIssue : unmatchedIssues) {
+ org.sonar.scanner.protocol.input.ScannerInput.ServerIssue unmatchedPreviousIssue = unmatchedIssue.getDto();
+ TrackedIssue unmatched = IssueTransformer.toTrackedIssue(unmatchedPreviousIssue);
+ if (unmatchedIssue.getRuleKey().isManual() && !Issue.STATUS_CLOSED.equals(unmatchedPreviousIssue.getStatus())) {
+ relocateManualIssue(unmatched, unmatchedIssue, sourceHashHolder);
+ }
+ updateUnmatchedIssue(unmatched, false /* manual issues can be kept open */);
+ mergeTo.add(unmatched);
+ }
+ }
+
+ private static void addUnmatchedFromReport(Iterable<TrackedIssue> rawIssues, Collection<TrackedIssue> trackedIssues, Date analysisDate) {
+ for (TrackedIssue rawIssue : rawIssues) {
+ rawIssue.setCreationDate(analysisDate);
+ trackedIssues.add(rawIssue);
+ }
+ }
+
+ private void addIssuesOnDeletedComponents(Collection<TrackedIssue> issues) {
+ for (org.sonar.scanner.protocol.input.ScannerInput.ServerIssue previous : serverIssueRepository.issuesOnMissingComponents()) {
+ TrackedIssue dead = IssueTransformer.toTrackedIssue(previous);
+ updateUnmatchedIssue(dead, true);
+ issues.add(dead);
+ }
+ }
+
+ private void updateUnmatchedIssue(TrackedIssue issue, boolean forceEndOfLife) {
+ ActiveRule activeRule = activeRules.find(issue.getRuleKey());
+ issue.setNew(false);
+
+ boolean manualIssue = issue.getRuleKey().isManual();
+ boolean isRemovedRule = activeRule == null;
+
+ if (isRemovedRule) {
+ IssueTransformer.resolveRemove(issue);
+ } else if (forceEndOfLife || !manualIssue) {
+ IssueTransformer.close(issue);
+ }
+ }
+
+ private static void relocateManualIssue(TrackedIssue newIssue, ServerIssueFromWs oldIssue, SourceHashHolder sourceHashHolder) {
+ Integer previousLine = oldIssue.getLine();
+ if (previousLine == null) {
+ return;
+ }
+
+ Collection<Integer> newLinesWithSameHash = sourceHashHolder.getNewLinesMatching(previousLine);
+ if (newLinesWithSameHash.isEmpty()) {
+ if (previousLine > sourceHashHolder.getHashedSource().length()) {
+ IssueTransformer.resolveRemove(newIssue);
+ }
+ } else if (newLinesWithSameHash.size() == 1) {
+ Integer newLine = newLinesWithSameHash.iterator().next();
+ newIssue.setStartLine(newLine);
+ newIssue.setEndLine(newLine);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java
new file mode 100644
index 00000000000..aaf2ba7bdf5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+/**
+ * Compute hashes of block around each line
+ */
+public class RollingFileHashes {
+
+ final int[] rollingHashes;
+
+ private RollingFileHashes(int[] hashes) {
+ this.rollingHashes = hashes;
+ }
+
+ public static RollingFileHashes create(FileHashes hashes, int halfBlockSize) {
+ int size = hashes.length();
+ int[] rollingHashes = new int[size];
+
+ RollingHashCalculator hashCalulator = new RollingHashCalculator(halfBlockSize * 2 + 1);
+ for (int i = 1; i <= Math.min(size, halfBlockSize + 1); i++) {
+ hashCalulator.add(hashes.getHash(i).hashCode());
+ }
+ for (int i = 1; i <= size; i++) {
+ rollingHashes[i - 1] = hashCalulator.getHash();
+ if (i - halfBlockSize > 0) {
+ hashCalulator.remove(hashes.getHash(i - halfBlockSize).hashCode());
+ }
+ if (i + 1 + halfBlockSize <= size) {
+ hashCalulator.add(hashes.getHash(i + 1 + halfBlockSize).hashCode());
+ } else {
+ hashCalulator.add(0);
+ }
+ }
+
+ return new RollingFileHashes(rollingHashes);
+ }
+
+ public int getHash(int line) {
+ return rollingHashes[line - 1];
+ }
+
+ private static class RollingHashCalculator {
+
+ private static final int PRIME_BASE = 31;
+
+ private final int power;
+ private int hash;
+
+ public RollingHashCalculator(int size) {
+ int pow = 1;
+ for (int i = 0; i < size - 1; i++) {
+ pow = pow * PRIME_BASE;
+ }
+ this.power = pow;
+ }
+
+ public void add(int value) {
+ hash = hash * PRIME_BASE + value;
+ }
+
+ public void remove(int value) {
+ hash = hash - power * value;
+ }
+
+ public int getHash() {
+ return hash;
+ }
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueFromWs.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueFromWs.java
new file mode 100644
index 00000000000..7898d5c88eb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueFromWs.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import javax.annotation.CheckForNull;
+
+import org.sonar.core.issue.tracking.Trackable;
+import org.sonar.api.rule.RuleKey;
+
+public class ServerIssueFromWs implements Trackable {
+
+ private org.sonar.scanner.protocol.input.ScannerInput.ServerIssue dto;
+
+ public ServerIssueFromWs(org.sonar.scanner.protocol.input.ScannerInput.ServerIssue dto) {
+ this.dto = dto;
+ }
+
+ public org.sonar.scanner.protocol.input.ScannerInput.ServerIssue getDto() {
+ return dto;
+ }
+
+ public String key() {
+ return dto.getKey();
+ }
+
+ @Override
+ public RuleKey getRuleKey() {
+ return RuleKey.of(dto.getRuleRepository(), dto.getRuleKey());
+ }
+
+ @Override
+ @CheckForNull
+ public String getLineHash() {
+ return dto.hasChecksum() ? dto.getChecksum() : null;
+ }
+
+ @Override
+ @CheckForNull
+ public Integer getLine() {
+ return dto.hasLine() ? dto.getLine() : null;
+ }
+
+ @Override
+ public String getMessage() {
+ return dto.hasMsg() ? dto.getMsg() : "";
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueRepository.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueRepository.java
new file mode 100644
index 00000000000..16f104583fd
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueRepository.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import com.google.common.base.Function;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.index.Cache;
+import org.sonar.batch.index.Caches;
+import org.sonar.batch.repository.ServerIssuesLoader;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+import org.sonar.core.component.ComponentKeys;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+@BatchSide
+public class ServerIssueRepository {
+
+ private static final Logger LOG = Loggers.get(ServerIssueRepository.class);
+ private static final String LOG_MSG = "Load server issues";
+
+ private final Caches caches;
+ private Cache<ServerIssue> issuesCache;
+ private final ServerIssuesLoader previousIssuesLoader;
+ private final ImmutableProjectReactor reactor;
+ private final BatchComponentCache resourceCache;
+
+ public ServerIssueRepository(Caches caches, ServerIssuesLoader previousIssuesLoader, ImmutableProjectReactor reactor, BatchComponentCache resourceCache) {
+ this.caches = caches;
+ this.previousIssuesLoader = previousIssuesLoader;
+ this.reactor = reactor;
+ this.resourceCache = resourceCache;
+ }
+
+ public void load() {
+ Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+ this.issuesCache = caches.createCache("previousIssues");
+ caches.registerValueCoder(ServerIssue.class, new ServerIssueValueCoder());
+ boolean fromCache = previousIssuesLoader.load(reactor.getRoot().getKeyWithBranch(), new SaveIssueConsumer());
+ profiler.stopInfo(fromCache);
+ }
+
+ public Iterable<ServerIssue> byComponent(BatchComponent component) {
+ return issuesCache.values(component.batchId());
+ }
+
+ private class SaveIssueConsumer implements Function<ServerIssue, Void> {
+
+ @Override
+ public Void apply(@Nullable ServerIssue issue) {
+ if (issue == null) {
+ return null;
+ }
+ String componentKey = ComponentKeys.createEffectiveKey(issue.getModuleKey(), issue.hasPath() ? issue.getPath() : null);
+ BatchComponent r = resourceCache.get(componentKey);
+ if (r == null) {
+ // Deleted resource
+ issuesCache.put(0, issue.getKey(), issue);
+ } else {
+ issuesCache.put(r.batchId(), issue.getKey(), issue);
+ }
+ return null;
+ }
+ }
+
+ public Iterable<ServerIssue> issuesOnMissingComponents() {
+ return issuesCache.values(0);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueValueCoder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueValueCoder.java
new file mode 100644
index 00000000000..44876e4761c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerIssueValueCoder.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import com.persistit.Value;
+import com.persistit.encoding.CoderContext;
+import com.persistit.encoding.ValueCoder;
+import java.io.IOException;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+
+public class ServerIssueValueCoder implements ValueCoder {
+
+ @Override
+ public void put(Value value, Object object, CoderContext context) {
+ ServerIssue issue = (ServerIssue) object;
+ value.putByteArray(issue.toByteArray());
+ }
+
+ @Override
+ public Object get(Value value, Class<?> clazz, CoderContext context) {
+ try {
+ return ServerIssue.parseFrom(value.getByteArray());
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to read issue from cache", e);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerLineHashesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerLineHashesLoader.java
new file mode 100644
index 00000000000..fb0b32d0d2b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/ServerLineHashesLoader.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+import javax.annotation.Nullable;
+
+import org.sonar.api.batch.BatchSide;
+
+@BatchSide
+public interface ServerLineHashesLoader {
+
+ String[] getLineHashes(String fileKey, @Nullable MutableBoolean fromCache);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.java
new file mode 100644
index 00000000000..583a92f0b9d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import com.google.common.collect.ImmutableSet;
+import org.sonar.api.batch.fs.InputFile.Status;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+
+import javax.annotation.CheckForNull;
+
+import java.util.Collection;
+
+public class SourceHashHolder {
+
+ private final ServerLineHashesLoader lastSnapshots;
+
+ private FileHashes hashedReference;
+ private FileHashes hashedSource;
+ private DefaultInputFile inputFile;
+
+ public SourceHashHolder(DefaultInputFile inputFile, ServerLineHashesLoader lastSnapshots) {
+ this.inputFile = inputFile;
+ this.lastSnapshots = lastSnapshots;
+ }
+
+ private void initHashes() {
+ if (hashedSource == null) {
+ hashedSource = FileHashes.create(inputFile);
+ Status status = inputFile.status();
+ if (status == Status.ADDED) {
+ hashedReference = null;
+ } else if (status == Status.SAME) {
+ hashedReference = hashedSource;
+ } else {
+ String[] lineHashes = lastSnapshots.getLineHashes(inputFile.key(), null);
+ hashedReference = lineHashes != null ? FileHashes.create(lineHashes) : null;
+ }
+ }
+ }
+
+ @CheckForNull
+ public FileHashes getHashedReference() {
+ initHashes();
+ return hashedReference;
+ }
+
+ public FileHashes getHashedSource() {
+ initHashes();
+ return hashedSource;
+ }
+
+ public Collection<Integer> getNewLinesMatching(Integer originLine) {
+ FileHashes reference = getHashedReference();
+ if (reference == null) {
+ return ImmutableSet.of();
+ } else {
+ return getHashedSource().getLinesForHash(reference.getHash(originLine));
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/TrackedIssue.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/TrackedIssue.java
new file mode 100644
index 00000000000..ea303bf6a4f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/TrackedIssue.java
@@ -0,0 +1,262 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import com.google.common.base.Preconditions;
+import java.io.Serializable;
+import java.util.Date;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.tracking.Trackable;
+
+public class TrackedIssue implements Trackable, Serializable {
+ private static final long serialVersionUID = -1755017079070964287L;
+
+ private RuleKey ruleKey;
+ private String key;
+ private String severity;
+ private Integer startLine;
+ private Integer startLineOffset;
+ private Integer endLine;
+ private Integer endLineOffset;
+ private Double gap;
+ private boolean isNew;
+ private Date creationDate;
+ private String resolution;
+ private String status;
+ private String assignee;
+ private String reporter;
+ private String componentKey;
+ private String message;
+
+ private transient FileHashes hashes;
+
+ public TrackedIssue() {
+ hashes = null;
+ }
+
+ public TrackedIssue(@Nullable FileHashes hashes) {
+ this.hashes = hashes;
+ }
+
+ @Override
+ @CheckForNull
+ public String getLineHash() {
+ if (getLine() == null || hashes == null) {
+ return null;
+ }
+
+ int line = getLine();
+ Preconditions.checkState(line <= hashes.length(), "Invalid line number for issue %s. File has only %s line(s)", this, hashes.length());
+
+ return hashes.getHash(line);
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ public TrackedIssue setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public String componentKey() {
+ return componentKey;
+ }
+
+ public TrackedIssue setComponentKey(String componentKey) {
+ this.componentKey = componentKey;
+ return this;
+ }
+
+ public String key() {
+ return key;
+ }
+
+ public Integer startLine() {
+ return startLine;
+ }
+
+ @Override
+ public Integer getLine() {
+ return startLine;
+ }
+
+ public TrackedIssue setStartLine(Integer startLine) {
+ this.startLine = startLine;
+ return this;
+ }
+
+ public Integer startLineOffset() {
+ return startLineOffset;
+ }
+
+ public TrackedIssue setStartLineOffset(Integer startLineOffset) {
+ this.startLineOffset = startLineOffset;
+ return this;
+ }
+
+ public Integer endLine() {
+ return endLine;
+ }
+
+ public TrackedIssue setEndLine(Integer endLine) {
+ this.endLine = endLine;
+ return this;
+ }
+
+ public Integer endLineOffset() {
+ return endLineOffset;
+ }
+
+ public TrackedIssue setEndLineOffset(Integer endLineOffset) {
+ this.endLineOffset = endLineOffset;
+ return this;
+ }
+
+ public TrackedIssue setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public String assignee() {
+ return assignee;
+ }
+
+ public TrackedIssue setAssignee(String assignee) {
+ this.assignee = assignee;
+ return this;
+ }
+
+ public String reporter() {
+ return reporter;
+ }
+
+ public TrackedIssue setReporter(String reporter) {
+ this.reporter = reporter;
+ return this;
+ }
+
+ public String resolution() {
+ return resolution;
+ }
+
+ public TrackedIssue setResolution(String resolution) {
+ this.resolution = resolution;
+ return this;
+ }
+
+ public String status() {
+ return status;
+ }
+
+ public TrackedIssue setStatus(String status) {
+ this.status = status;
+ return this;
+ }
+
+ @Override
+ public RuleKey getRuleKey() {
+ return ruleKey;
+ }
+
+ public String severity() {
+ return severity;
+ }
+
+ public Double gap() {
+ return gap;
+ }
+
+ public Date getCreationDate() {
+ return creationDate;
+ }
+
+ public boolean isNew() {
+ return isNew;
+ }
+
+ public TrackedIssue setNew(boolean isNew) {
+ this.isNew = isNew;
+ return this;
+ }
+
+ public Date creationDate() {
+ return creationDate;
+ }
+
+ public TrackedIssue setCreationDate(Date creationDate) {
+ this.creationDate = creationDate;
+ return this;
+ }
+
+ public TrackedIssue setRuleKey(RuleKey ruleKey) {
+ this.ruleKey = ruleKey;
+ return this;
+ }
+
+ public TrackedIssue setSeverity(String severity) {
+ this.severity = severity;
+ return this;
+ }
+
+ public TrackedIssue setGap(Double gap) {
+ this.gap = gap;
+ return this;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((key == null) ? 0 : key.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackedIssue other = (TrackedIssue) obj;
+ if (key == null) {
+ if (other.key != null) {
+ return false;
+ }
+ } else if (!key.equals(other.key)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/package-info.java
new file mode 100644
index 00000000000..148dc77f0f4
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/issue/tracking/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.issue.tracking;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/FakePluginInstaller.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/FakePluginInstaller.java
new file mode 100644
index 00000000000..71205c7b08d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/FakePluginInstaller.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import org.sonar.api.SonarPlugin;
+import org.sonar.batch.bootstrap.PluginInstaller;
+import org.sonar.core.platform.PluginInfo;
+
+public class FakePluginInstaller implements PluginInstaller {
+ public static final String MEDIUM_TEST_ENABLED = "sonar.mediumTest.enabled";
+
+ private final Map<String, PluginInfo> infosByKeys = new HashMap<>();
+ private final Map<String, SonarPlugin> instancesByKeys = new HashMap<>();
+
+ public FakePluginInstaller add(String pluginKey, File jarFile) {
+ infosByKeys.put(pluginKey, PluginInfo.create(jarFile));
+ return this;
+ }
+
+ public FakePluginInstaller add(String pluginKey, SonarPlugin instance) {
+ instancesByKeys.put(pluginKey, instance);
+ return this;
+ }
+
+ @Override
+ public Map<String, PluginInfo> installRemotes() {
+ return infosByKeys;
+ }
+
+ @Override
+ public Map<String, SonarPlugin> installLocals() {
+ return instancesByKeys;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObserver.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObserver.java
new file mode 100644
index 00000000000..05b9f338eef
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObserver.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.ExtensionPoint;
+import org.sonar.batch.scan.ProjectScanContainer;
+
+@BatchSide
+@ExtensionPoint
+public interface ScanTaskObserver {
+
+ void scanTaskCompleted(ProjectScanContainer container);
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObservers.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObservers.java
new file mode 100644
index 00000000000..90e092cad77
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/ScanTaskObservers.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest;
+
+import org.sonar.batch.scan.ProjectScanContainer;
+
+public class ScanTaskObservers {
+
+ private ScanTaskObserver[] observers;
+ private ProjectScanContainer projectScanContainer;
+
+ public ScanTaskObservers(ProjectScanContainer projectScanContainer, ScanTaskObserver... observers) {
+ this.projectScanContainer = projectScanContainer;
+ this.observers = observers;
+ }
+
+ public ScanTaskObservers(ProjectScanContainer projectScanContainer) {
+ this(projectScanContainer, new ScanTaskObserver[0]);
+ }
+
+ public void notifyEndOfScanTask() {
+ for (ScanTaskObserver scanTaskObserver : observers) {
+ scanTaskObserver.scanTaskCompleted(projectScanContainer);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/TaskResult.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/TaskResult.java
new file mode 100644
index 00000000000..5d3d87b540d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/TaskResult.java
@@ -0,0 +1,292 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest;
+
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Lists;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextPointer;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import org.sonar.batch.report.ScannerReportUtils;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.scan.ProjectScanContainer;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReport.Component;
+import org.sonar.scanner.protocol.output.ScannerReport.Metadata;
+import org.sonar.scanner.protocol.output.ScannerReport.Symbol;
+
+public class TaskResult implements org.sonar.batch.mediumtest.ScanTaskObserver {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TaskResult.class);
+
+ private List<TrackedIssue> issues = new ArrayList<>();
+ private Map<String, InputFile> inputFiles = new HashMap<>();
+ private Map<String, Component> reportComponents = new HashMap<>();
+ private Map<String, InputDir> inputDirs = new HashMap<>();
+ private ScannerReportReader reader;
+
+ @Override
+ public void scanTaskCompleted(ProjectScanContainer container) {
+ LOG.info("Store analysis results in memory for later assertions in medium test");
+ for (TrackedIssue issue : container.getComponentByType(IssueCache.class).all()) {
+ issues.add(issue);
+ }
+
+ ReportPublisher reportPublisher = container.getComponentByType(ReportPublisher.class);
+ reader = new ScannerReportReader(reportPublisher.getReportDir());
+ if (!container.getComponentByType(AnalysisMode.class).isIssues()) {
+ Metadata readMetadata = getReportReader().readMetadata();
+ int rootComponentRef = readMetadata.getRootComponentRef();
+ storeReportComponents(rootComponentRef, null, readMetadata.hasBranch() ? readMetadata.getBranch() : null);
+ }
+
+ storeFs(container);
+
+ }
+
+ private void storeReportComponents(int componentRef, String parentModuleKey, @Nullable String branch) {
+ Component component = getReportReader().readComponent(componentRef);
+ if (component.hasKey()) {
+ reportComponents.put(component.getKey() + (branch != null ? ":" + branch : ""), component);
+ } else {
+ reportComponents.put(parentModuleKey + (branch != null ? ":" + branch : "") + ":" + component.getPath(), component);
+ }
+ for (int childId : component.getChildRefList()) {
+ storeReportComponents(childId, component.hasKey() ? component.getKey() : parentModuleKey, branch);
+ }
+
+ }
+
+ public ScannerReportReader getReportReader() {
+ return reader;
+ }
+
+ private void storeFs(ProjectScanContainer container) {
+ InputPathCache inputFileCache = container.getComponentByType(InputPathCache.class);
+ for (InputFile inputPath : inputFileCache.allFiles()) {
+ inputFiles.put(inputPath.relativePath(), inputPath);
+ }
+ for (InputDir inputPath : inputFileCache.allDirs()) {
+ inputDirs.put(inputPath.relativePath(), inputPath);
+ }
+ }
+
+ public List<TrackedIssue> trackedIssues() {
+ return issues;
+ }
+
+ public Component getReportComponent(String key) {
+ return reportComponents.get(key);
+ }
+
+ public List<ScannerReport.Issue> issuesFor(InputComponent inputComponent) {
+ int ref = reportComponents.get(inputComponent.key()).getRef();
+ return issuesFor(ref);
+ }
+
+ public List<ScannerReport.Issue> issuesFor(Component reportComponent) {
+ int ref = reportComponent.getRef();
+ return issuesFor(ref);
+ }
+
+ private List<ScannerReport.Issue> issuesFor(int ref) {
+ List<ScannerReport.Issue> result = Lists.newArrayList();
+ try (CloseableIterator<ScannerReport.Issue> it = reader.readComponentIssues(ref)) {
+ while (it.hasNext()) {
+ result.add(it.next());
+ }
+ }
+ return result;
+ }
+
+ public Collection<InputFile> inputFiles() {
+ return inputFiles.values();
+ }
+
+ @CheckForNull
+ public InputFile inputFile(String relativePath) {
+ return inputFiles.get(relativePath);
+ }
+
+ public Collection<InputDir> inputDirs() {
+ return inputDirs.values();
+ }
+
+ @CheckForNull
+ public InputDir inputDir(String relativePath) {
+ return inputDirs.get(relativePath);
+ }
+
+ public Map<String, List<ScannerReport.Measure>> allMeasures() {
+ Map<String, List<ScannerReport.Measure>> result = new HashMap<>();
+ for (Map.Entry<String, Component> component : reportComponents.entrySet()) {
+ List<ScannerReport.Measure> measures = new ArrayList<>();
+ try (CloseableIterator<ScannerReport.Measure> it = reader.readComponentMeasures(component.getValue().getRef())) {
+ Iterators.addAll(measures, it);
+ }
+ result.put(component.getKey(), measures);
+ }
+ return result;
+ }
+
+ /**
+ * Get highlighting types at a given position in an inputfile
+ * @param lineOffset 0-based offset in file
+ */
+ public List<TypeOfText> highlightingTypeFor(InputFile file, int line, int lineOffset) {
+ int ref = reportComponents.get(((DefaultInputFile) file).key()).getRef();
+ if (!reader.hasSyntaxHighlighting(ref)) {
+ return Collections.emptyList();
+ }
+ TextPointer pointer = file.newPointer(line, lineOffset);
+ List<TypeOfText> result = new ArrayList<>();
+ try (CloseableIterator<ScannerReport.SyntaxHighlighting> it = reader.readComponentSyntaxHighlighting(ref)) {
+ while (it.hasNext()) {
+ ScannerReport.SyntaxHighlighting rule = it.next();
+ TextRange ruleRange = toRange(file, rule.getRange());
+ if (ruleRange.start().compareTo(pointer) <= 0 && ruleRange.end().compareTo(pointer) > 0) {
+ result.add(ScannerReportUtils.toBatchType(rule.getType()));
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Can't read syntax highlighting for " + file.absolutePath(), e);
+ }
+ return result;
+ }
+
+ private static TextRange toRange(InputFile file, ScannerReport.TextRange reportRange) {
+ return file.newRange(file.newPointer(reportRange.getStartLine(), reportRange.getStartOffset()), file.newPointer(reportRange.getEndLine(), reportRange.getEndOffset()));
+ }
+
+ /**
+ * Get list of all start positions of a symbol in an inputfile
+ * @param symbolStartLine 0-based start offset for the symbol in file
+ * @param symbolStartLineOffset 0-based end offset for the symbol in file
+ */
+ @CheckForNull
+ public List<ScannerReport.TextRange> symbolReferencesFor(InputFile file, int symbolStartLine, int symbolStartLineOffset) {
+ int ref = reportComponents.get(((DefaultInputFile) file).key()).getRef();
+ try (CloseableIterator<Symbol> symbols = getReportReader().readComponentSymbols(ref)) {
+ while (symbols.hasNext()) {
+ Symbol symbol = symbols.next();
+ if (symbol.getDeclaration().getStartLine() == symbolStartLine && symbol.getDeclaration().getStartOffset() == symbolStartLineOffset) {
+ return symbol.getReferenceList();
+ }
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ public List<ScannerReport.Duplication> duplicationsFor(InputFile file) {
+ List<ScannerReport.Duplication> result = new ArrayList<>();
+ int ref = reportComponents.get(((DefaultInputFile) file).key()).getRef();
+ try (CloseableIterator<ScannerReport.Duplication> it = getReportReader().readComponentDuplications(ref)) {
+ while (it.hasNext()) {
+ result.add(it.next());
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return result;
+ }
+
+ public List<ScannerReport.CpdTextBlock> duplicationBlocksFor(InputFile file) {
+ List<ScannerReport.CpdTextBlock> result = new ArrayList<>();
+ int ref = reportComponents.get(((DefaultInputFile) file).key()).getRef();
+ try (CloseableIterator<ScannerReport.CpdTextBlock> it = getReportReader().readCpdTextBlocks(ref)) {
+ while (it.hasNext()) {
+ result.add(it.next());
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return result;
+ }
+
+ @CheckForNull
+ public ScannerReport.Coverage coverageFor(InputFile file, int line) {
+ int ref = reportComponents.get(((DefaultInputFile) file).key()).getRef();
+ try (CloseableIterator<ScannerReport.Coverage> it = getReportReader().readComponentCoverage(ref)) {
+ while (it.hasNext()) {
+ ScannerReport.Coverage coverage = it.next();
+ if (coverage.getLine() == line) {
+ return coverage;
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return null;
+ }
+
+ public ScannerReport.Test testExecutionFor(InputFile testFile, String testName) {
+ int ref = reportComponents.get(((DefaultInputFile) testFile).key()).getRef();
+ try (InputStream inputStream = FileUtils.openInputStream(getReportReader().readTests(ref))) {
+ ScannerReport.Test test = ScannerReport.Test.PARSER.parseDelimitedFrom(inputStream);
+ while (test != null) {
+ if (test.getName().equals(testName)) {
+ return test;
+ }
+ test = ScannerReport.Test.PARSER.parseDelimitedFrom(inputStream);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return null;
+ }
+
+ public ScannerReport.CoverageDetail coveragePerTestFor(InputFile testFile, String testName) {
+ int ref = reportComponents.get(((DefaultInputFile) testFile).key()).getRef();
+ try (InputStream inputStream = FileUtils.openInputStream(getReportReader().readCoverageDetails(ref))) {
+ ScannerReport.CoverageDetail details = ScannerReport.CoverageDetail.PARSER.parseDelimitedFrom(inputStream);
+ while (details != null) {
+ if (details.getTestName().equals(testName)) {
+ return details;
+ }
+ details = ScannerReport.CoverageDetail.PARSER.parseDelimitedFrom(inputStream);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return null;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/package-info.java
new file mode 100644
index 00000000000..2cc10ab66ff
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/mediumtest/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.mediumtest;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/package-info.java
new file mode 100644
index 00000000000..408dd9f93a6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseEvent.java
new file mode 100644
index 00000000000..df8bc690534
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseEvent.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.events.EventHandler;
+import org.sonar.batch.events.BatchEvent;
+
+public abstract class AbstractPhaseEvent<H extends EventHandler> extends BatchEvent<H> {
+
+ private final boolean start;
+
+ public AbstractPhaseEvent(boolean start) {
+ this.start = start;
+ }
+
+ public final boolean isStart() {
+ return start;
+ }
+
+ public final boolean isEnd() {
+ return !start;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseExecutor.java
new file mode 100644
index 00000000000..c388b75f1f3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/AbstractPhaseExecutor.java
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.events.BatchStepEvent;
+import org.sonar.batch.events.EventBus;
+import org.sonar.batch.index.DefaultIndex;
+import org.sonar.batch.issue.ignore.scanner.IssueExclusionsLoader;
+import org.sonar.batch.rule.QProfileVerifier;
+import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem;
+import org.sonar.batch.scan.filesystem.FileSystemLogger;
+
+public abstract class AbstractPhaseExecutor {
+
+ private final EventBus eventBus;
+ private final PostJobsExecutor postJobsExecutor;
+ private final InitializersExecutor initializersExecutor;
+ private final SensorsExecutor sensorsExecutor;
+ private final SensorContext sensorContext;
+ private final DefaultIndex index;
+ private final ProjectInitializer pi;
+ private final FileSystemLogger fsLogger;
+ private final DefaultModuleFileSystem fs;
+ private final QProfileVerifier profileVerifier;
+ private final IssueExclusionsLoader issueExclusionsLoader;
+
+ public AbstractPhaseExecutor(InitializersExecutor initializersExecutor, PostJobsExecutor postJobsExecutor, SensorsExecutor sensorsExecutor,
+ SensorContext sensorContext, DefaultIndex index,
+ EventBus eventBus, ProjectInitializer pi,
+ FileSystemLogger fsLogger, DefaultModuleFileSystem fs, QProfileVerifier profileVerifier,
+ IssueExclusionsLoader issueExclusionsLoader) {
+ this.postJobsExecutor = postJobsExecutor;
+ this.initializersExecutor = initializersExecutor;
+ this.sensorsExecutor = sensorsExecutor;
+ this.sensorContext = sensorContext;
+ this.index = index;
+ this.eventBus = eventBus;
+ this.pi = pi;
+ this.fsLogger = fsLogger;
+ this.fs = fs;
+ this.profileVerifier = profileVerifier;
+ this.issueExclusionsLoader = issueExclusionsLoader;
+ }
+
+ /**
+ * Executed on each module
+ */
+ public final void execute(Project module) {
+ pi.execute(module);
+
+ eventBus.fireEvent(new ProjectAnalysisEvent(module, true));
+
+ executeInitializersPhase();
+
+ // Index and lock the filesystem
+ indexFs();
+
+ // Log detected languages and their profiles after FS is indexed and languages detected
+ profileVerifier.execute();
+
+ // Initialize issue exclusions
+ initIssueExclusions();
+
+ sensorsExecutor.execute(sensorContext);
+
+ if (module.isRoot()) {
+ executeOnRoot();
+ postJobsExecutor.execute(sensorContext);
+ }
+ cleanMemory();
+ eventBus.fireEvent(new ProjectAnalysisEvent(module, false));
+ }
+
+ protected abstract void executeOnRoot();
+
+ private void initIssueExclusions() {
+ String stepName = "Init issue exclusions";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ issueExclusionsLoader.execute();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+
+ private void indexFs() {
+ String stepName = "Index filesystem";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ fs.index();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+
+ private void executeInitializersPhase() {
+ initializersExecutor.execute();
+ fsLogger.log();
+ }
+
+ private void cleanMemory() {
+ String cleanMemory = "Clean memory";
+ eventBus.fireEvent(new BatchStepEvent(cleanMemory, true));
+ index.clear();
+ eventBus.fireEvent(new BatchStepEvent(cleanMemory, false));
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializerExecutionEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializerExecutionEvent.java
new file mode 100644
index 00000000000..9a8bdc92706
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializerExecutionEvent.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.Initializer;
+import org.sonar.api.batch.events.InitializerExecutionHandler;
+
+class InitializerExecutionEvent extends AbstractPhaseEvent<InitializerExecutionHandler>
+ implements org.sonar.api.batch.events.InitializerExecutionHandler.InitializerExecutionEvent {
+
+ private final Initializer initializer;
+
+ InitializerExecutionEvent(Initializer initializer, boolean start) {
+ super(start);
+ this.initializer = initializer;
+ }
+
+ @Override
+ public Initializer getInitializer() {
+ return initializer;
+ }
+
+ @Override
+ public void dispatch(InitializerExecutionHandler handler) {
+ handler.onInitializerExecution(this);
+ }
+
+ @Override
+ public Class getType() {
+ return InitializerExecutionHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersExecutor.java
new file mode 100644
index 00000000000..591969ea1b0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersExecutor.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.Initializer;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.batch.bootstrap.BatchExtensionDictionnary;
+import org.sonar.batch.events.EventBus;
+
+import java.util.Collection;
+
+public class InitializersExecutor {
+
+ private static final Logger LOG = Loggers.get(SensorsExecutor.class);
+
+ private Project project;
+ private BatchExtensionDictionnary selector;
+ private EventBus eventBus;
+
+ public InitializersExecutor(BatchExtensionDictionnary selector, Project project, EventBus eventBus) {
+ this.selector = selector;
+ this.project = project;
+ this.eventBus = eventBus;
+ }
+
+ public void execute() {
+ Collection<Initializer> initializers = selector.select(Initializer.class, project, true, null);
+ eventBus.fireEvent(new InitializersPhaseEvent(Lists.newArrayList(initializers), true));
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Initializers : {}", StringUtils.join(initializers, " -> "));
+ }
+
+ for (Initializer initializer : initializers) {
+ eventBus.fireEvent(new InitializerExecutionEvent(initializer, true));
+
+ Profiler profiler = Profiler.create(LOG).startInfo("Initializer " + initializer);
+ initializer.execute(project);
+ profiler.stopInfo();
+ eventBus.fireEvent(new InitializerExecutionEvent(initializer, false));
+ }
+
+ eventBus.fireEvent(new InitializersPhaseEvent(Lists.newArrayList(initializers), false));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersPhaseEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersPhaseEvent.java
new file mode 100644
index 00000000000..5e109625da6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/InitializersPhaseEvent.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.Initializer;
+import org.sonar.api.batch.events.InitializersPhaseHandler;
+
+import java.util.List;
+
+class InitializersPhaseEvent extends AbstractPhaseEvent<InitializersPhaseHandler>
+ implements org.sonar.api.batch.events.InitializersPhaseHandler.InitializersPhaseEvent {
+
+ private final List<Initializer> initializers;
+
+ InitializersPhaseEvent(List<Initializer> initializers, boolean start) {
+ super(start);
+ this.initializers = initializers;
+ }
+
+ @Override
+ public List<Initializer> getInitializers() {
+ return initializers;
+ }
+
+ @Override
+ protected void dispatch(InitializersPhaseHandler handler) {
+ handler.onInitializersPhase(this);
+ }
+
+ @Override
+ protected Class getType() {
+ return InitializersPhaseHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/IssuesPhaseExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/IssuesPhaseExecutor.java
new file mode 100644
index 00000000000..ba9a155962e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/IssuesPhaseExecutor.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.batch.events.BatchStepEvent;
+import org.sonar.batch.events.EventBus;
+import org.sonar.batch.index.DefaultIndex;
+import org.sonar.batch.issue.IssueCallback;
+import org.sonar.batch.issue.ignore.scanner.IssueExclusionsLoader;
+import org.sonar.batch.issue.tracking.IssueTransition;
+import org.sonar.batch.rule.QProfileVerifier;
+import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem;
+import org.sonar.batch.scan.filesystem.FileSystemLogger;
+import org.sonar.batch.scan.report.IssuesReports;
+
+public final class IssuesPhaseExecutor extends AbstractPhaseExecutor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(IssuesPhaseExecutor.class);
+
+ private final EventBus eventBus;
+ private final IssuesReports issuesReport;
+ private final IssueTransition localIssueTracking;
+ private final IssueCallback issueCallback;
+
+ public IssuesPhaseExecutor(InitializersExecutor initializersExecutor, PostJobsExecutor postJobsExecutor, SensorsExecutor sensorsExecutor, SensorContext sensorContext,
+ DefaultIndex index, EventBus eventBus, ProjectInitializer pi, FileSystemLogger fsLogger, IssuesReports jsonReport, DefaultModuleFileSystem fs, QProfileVerifier profileVerifier,
+ IssueExclusionsLoader issueExclusionsLoader, IssueTransition localIssueTracking, IssueCallback issueCallback) {
+ super(initializersExecutor, postJobsExecutor, sensorsExecutor, sensorContext, index, eventBus, pi, fsLogger, fs, profileVerifier, issueExclusionsLoader);
+ this.eventBus = eventBus;
+ this.issuesReport = jsonReport;
+ this.localIssueTracking = localIssueTracking;
+ this.issueCallback = issueCallback;
+ }
+
+ @Override
+ protected void executeOnRoot() {
+ localIssueTracking();
+ issuesCallback();
+ issuesReport();
+ LOG.info("ANALYSIS SUCCESSFUL");
+ }
+
+ private void localIssueTracking() {
+ String stepName = "Local Issue Tracking";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ localIssueTracking.execute();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+
+ private void issuesCallback() {
+ String stepName = "Issues Callback";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ issueCallback.execute();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+
+ private void issuesReport() {
+ String stepName = "Issues Reports";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ issuesReport.execute();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PhasesTimeProfiler.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PhasesTimeProfiler.java
new file mode 100644
index 00000000000..ba8c7c498d0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PhasesTimeProfiler.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+
+import org.sonar.batch.util.BatchUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.events.SensorExecutionHandler;
+import org.sonar.api.batch.events.SensorsPhaseHandler;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+
+public class PhasesTimeProfiler implements SensorExecutionHandler, SensorsPhaseHandler {
+
+ private static final Logger LOG = Loggers.get(PhasesTimeProfiler.class);
+
+ private Profiler profiler = Profiler.create(LOG);
+
+ @Override
+ public void onSensorsPhase(SensorsPhaseEvent event) {
+ if (event.isStart()) {
+ LOG.debug("Sensors : {}", StringUtils.join(event.getSensors(), " -> "));
+ }
+ }
+
+ @Override
+ public void onSensorExecution(SensorExecutionEvent event) {
+ if (event.isStart()) {
+ profiler.startInfo("Sensor " + BatchUtils.describe(event.getSensor()));
+ } else {
+ profiler.stopInfo();
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobExecutionEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobExecutionEvent.java
new file mode 100644
index 00000000000..b7cd92a217b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobExecutionEvent.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.PostJob;
+import org.sonar.api.batch.events.PostJobExecutionHandler;
+
+class PostJobExecutionEvent extends AbstractPhaseEvent<PostJobExecutionHandler>
+ implements org.sonar.api.batch.events.PostJobExecutionHandler.PostJobExecutionEvent {
+
+ private final PostJob postJob;
+
+ PostJobExecutionEvent(PostJob postJob, boolean start) {
+ super(start);
+ this.postJob = postJob;
+ }
+
+ @Override
+ public PostJob getPostJob() {
+ return postJob;
+ }
+
+ @Override
+ public void dispatch(PostJobExecutionHandler handler) {
+ handler.onPostJobExecution(this);
+ }
+
+ @Override
+ public Class getType() {
+ return PostJobExecutionHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobPhaseEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobPhaseEvent.java
new file mode 100644
index 00000000000..860d7cc938d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobPhaseEvent.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.PostJob;
+import org.sonar.api.batch.events.PostJobsPhaseHandler;
+
+import java.util.List;
+
+class PostJobPhaseEvent extends AbstractPhaseEvent<PostJobsPhaseHandler>
+ implements org.sonar.api.batch.events.PostJobsPhaseHandler.PostJobsPhaseEvent {
+
+ private final List<PostJob> postJobs;
+
+ PostJobPhaseEvent(List<PostJob> postJobs, boolean start) {
+ super(start);
+ this.postJobs = postJobs;
+ }
+
+ @Override
+ public List<PostJob> getPostJobs() {
+ return postJobs;
+ }
+
+ @Override
+ protected void dispatch(PostJobsPhaseHandler handler) {
+ handler.onPostJobsPhase(this);
+ }
+
+ @Override
+ protected Class getType() {
+ return PostJobsPhaseHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobsExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobsExecutor.java
new file mode 100644
index 00000000000..277b45b8d3c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PostJobsExecutor.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.batch.util.BatchUtils;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.PostJob;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.bootstrap.BatchExtensionDictionnary;
+import org.sonar.batch.events.EventBus;
+
+import java.util.Collection;
+
+@BatchSide
+public class PostJobsExecutor {
+ private static final Logger LOG = LoggerFactory.getLogger(PostJobsExecutor.class);
+
+ private final BatchExtensionDictionnary selector;
+ private final Project project;
+ private final EventBus eventBus;
+
+ public PostJobsExecutor(BatchExtensionDictionnary selector, Project project, EventBus eventBus) {
+ this.selector = selector;
+ this.project = project;
+ this.eventBus = eventBus;
+ }
+
+ public void execute(SensorContext context) {
+ Collection<PostJob> postJobs = selector.select(PostJob.class, project, true, null);
+
+ eventBus.fireEvent(new PostJobPhaseEvent(Lists.newArrayList(postJobs), true));
+ execute(context, postJobs);
+ eventBus.fireEvent(new PostJobPhaseEvent(Lists.newArrayList(postJobs), false));
+ }
+
+ private void execute(SensorContext context, Collection<PostJob> postJobs) {
+ logPostJobs(postJobs);
+
+ for (PostJob postJob : postJobs) {
+ LOG.info("Executing post-job {}", BatchUtils.describe(postJob));
+ eventBus.fireEvent(new PostJobExecutionEvent(postJob, true));
+ postJob.executeOn(project, context);
+ eventBus.fireEvent(new PostJobExecutionEvent(postJob, false));
+ }
+ }
+
+ private static void logPostJobs(Collection<PostJob> postJobs) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Post-jobs : {}", StringUtils.join(postJobs, " -> "));
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectAnalysisEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectAnalysisEvent.java
new file mode 100644
index 00000000000..428774acd82
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectAnalysisEvent.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.events.ProjectAnalysisHandler;
+import org.sonar.api.resources.Project;
+
+class ProjectAnalysisEvent extends AbstractPhaseEvent<ProjectAnalysisHandler>
+ implements org.sonar.api.batch.events.ProjectAnalysisHandler.ProjectAnalysisEvent {
+
+ private final Project project;
+
+ ProjectAnalysisEvent(Project project, boolean start) {
+ super(start);
+ this.project = project;
+ }
+
+ @Override
+ public Project getProject() {
+ return project;
+ }
+
+ @Override
+ protected void dispatch(ProjectAnalysisHandler handler) {
+ handler.onProjectAnalysis(this);
+ }
+
+ @Override
+ protected Class getType() {
+ return ProjectAnalysisHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectInitializer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectInitializer.java
new file mode 100644
index 00000000000..8dac11520e5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/ProjectInitializer.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.BatchSide;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.MessageException;
+
+/**
+ * Should be dropped when org.sonar.api.resources.Project is fully refactored.
+ */
+@BatchSide
+public class ProjectInitializer {
+
+ private Languages languages;
+ private Settings settings;
+
+ public ProjectInitializer(Settings settings, Languages languages) {
+ this.settings = settings;
+ this.languages = languages;
+ }
+
+ public void execute(Project project) {
+ if (project.getLanguage() == null) {
+ initDeprecatedLanguage(project);
+ }
+ }
+
+ private void initDeprecatedLanguage(Project project) {
+ String languageKey = settings.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY);
+ if (StringUtils.isNotBlank(languageKey)) {
+ Language language = languages.get(languageKey);
+ if (language == null) {
+ throw MessageException.of("Language with key '" + languageKey + "' not found");
+ }
+ project.setLanguage(language);
+ } else {
+ project.setLanguage(Project.NONE_LANGUAGE);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PublishPhaseExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PublishPhaseExecutor.java
new file mode 100644
index 00000000000..33073a110b3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/PublishPhaseExecutor.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.SensorContext;
+import org.sonar.batch.cpd.CpdExecutor;
+import org.sonar.batch.events.BatchStepEvent;
+import org.sonar.batch.events.EventBus;
+import org.sonar.batch.index.DefaultIndex;
+import org.sonar.batch.issue.ignore.scanner.IssueExclusionsLoader;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.rule.QProfileVerifier;
+import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem;
+import org.sonar.batch.scan.filesystem.FileSystemLogger;
+
+public final class PublishPhaseExecutor extends AbstractPhaseExecutor {
+
+ private final EventBus eventBus;
+ private final ReportPublisher reportPublisher;
+ private final CpdExecutor cpdExecutor;
+
+ public PublishPhaseExecutor(InitializersExecutor initializersExecutor, PostJobsExecutor postJobsExecutor, SensorsExecutor sensorsExecutor, SensorContext sensorContext,
+ DefaultIndex index, EventBus eventBus, ReportPublisher reportPublisher, ProjectInitializer pi, FileSystemLogger fsLogger, DefaultModuleFileSystem fs,
+ QProfileVerifier profileVerifier, IssueExclusionsLoader issueExclusionsLoader, CpdExecutor cpdExecutor) {
+ super(initializersExecutor, postJobsExecutor, sensorsExecutor, sensorContext, index, eventBus, pi, fsLogger, fs, profileVerifier, issueExclusionsLoader);
+ this.eventBus = eventBus;
+ this.reportPublisher = reportPublisher;
+ this.cpdExecutor = cpdExecutor;
+ }
+
+ @Override
+ protected void executeOnRoot() {
+ computeDuplications();
+ publishReportJob();
+ }
+
+ private void computeDuplications() {
+ String stepName = "Computing duplications";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ cpdExecutor.execute();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+
+ private void publishReportJob() {
+ String stepName = "Publish report";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ this.reportPublisher.execute();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorExecutionEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorExecutionEvent.java
new file mode 100644
index 00000000000..22cbd25aadb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorExecutionEvent.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.events.SensorExecutionHandler;
+
+class SensorExecutionEvent extends AbstractPhaseEvent<SensorExecutionHandler>
+ implements org.sonar.api.batch.events.SensorExecutionHandler.SensorExecutionEvent {
+
+ private final Sensor sensor;
+
+ SensorExecutionEvent(Sensor sensor, boolean start) {
+ super(start);
+ this.sensor = sensor;
+ }
+
+ @Override
+ public Sensor getSensor() {
+ return sensor;
+ }
+
+ @Override
+ public void dispatch(SensorExecutionHandler handler) {
+ handler.onSensorExecution(this);
+ }
+
+ @Override
+ public Class getType() {
+ return SensorExecutionHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsExecutor.java
new file mode 100644
index 00000000000..3af1cf2b6ee
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsExecutor.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.bootstrap.BatchExtensionDictionnary;
+import org.sonar.batch.events.EventBus;
+
+import java.util.Collection;
+
+@BatchSide
+public class SensorsExecutor {
+
+ private EventBus eventBus;
+ private Project module;
+ private BatchExtensionDictionnary selector;
+
+ public SensorsExecutor(BatchExtensionDictionnary selector, Project project, EventBus eventBus) {
+ this.selector = selector;
+ this.eventBus = eventBus;
+ this.module = project;
+ }
+
+ public void execute(SensorContext context) {
+ Collection<Sensor> sensors = selector.select(Sensor.class, module, true, null);
+ eventBus.fireEvent(new SensorsPhaseEvent(Lists.newArrayList(sensors), true));
+
+ for (Sensor sensor : sensors) {
+ executeSensor(context, sensor);
+ }
+
+ eventBus.fireEvent(new SensorsPhaseEvent(Lists.newArrayList(sensors), false));
+ }
+
+ private void executeSensor(SensorContext context, Sensor sensor) {
+ eventBus.fireEvent(new SensorExecutionEvent(sensor, true));
+ sensor.analyse(module, context);
+ eventBus.fireEvent(new SensorExecutionEvent(sensor, false));
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsPhaseEvent.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsPhaseEvent.java
new file mode 100644
index 00000000000..a1f658efa1e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/SensorsPhaseEvent.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.events.SensorsPhaseHandler;
+
+import java.util.List;
+
+class SensorsPhaseEvent extends AbstractPhaseEvent<SensorsPhaseHandler>
+ implements org.sonar.api.batch.events.SensorsPhaseHandler.SensorsPhaseEvent {
+
+ private final List<Sensor> sensors;
+
+ SensorsPhaseEvent(List<Sensor> sensors, boolean start) {
+ super(start);
+ this.sensors = sensors;
+ }
+
+ @Override
+ public List<Sensor> getSensors() {
+ return sensors;
+ }
+
+ @Override
+ protected void dispatch(SensorsPhaseHandler handler) {
+ handler.onSensorsPhase(this);
+ }
+
+ @Override
+ protected Class getType() {
+ return SensorsPhaseHandler.class;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/package-info.java
new file mode 100644
index 00000000000..f4cd97fca3c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/phases/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.phases;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/platform/DefaultServer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/platform/DefaultServer.java
new file mode 100644
index 00000000000..3136f4a3112
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/platform/DefaultServer.java
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.platform;
+
+import java.io.File;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import javax.annotation.CheckForNull;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.config.Settings;
+import org.sonar.api.platform.Server;
+import org.sonar.batch.bootstrap.GlobalProperties;
+
+@BatchSide
+public class DefaultServer extends Server {
+
+ private Settings settings;
+ private GlobalProperties props;
+
+ public DefaultServer(Settings settings, GlobalProperties props) {
+ this.settings = settings;
+ this.props = props;
+ }
+
+ @Override
+ public String getId() {
+ return settings.getString(CoreProperties.SERVER_ID);
+ }
+
+ @Override
+ public String getVersion() {
+ return settings.getString(CoreProperties.SERVER_VERSION);
+ }
+
+ @Override
+ public Date getStartedAt() {
+ String dateString = settings.getString(CoreProperties.SERVER_STARTTIME);
+ if (dateString != null) {
+ try {
+ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateString);
+
+ } catch (ParseException e) {
+ LoggerFactory.getLogger(getClass()).error("The property " + CoreProperties.SERVER_STARTTIME + " is badly formatted.", e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public File getRootDir() {
+ return null;
+ }
+
+ @Override
+ @CheckForNull
+ public File getDeployDir() {
+ return null;
+ }
+
+ @Override
+ public String getContextPath() {
+ return "";
+ }
+
+ @Override
+ public String getPublicRootUrl() {
+ return null;
+ }
+
+ @Override
+ public boolean isDev() {
+ return false;
+ }
+
+ @Override
+ public boolean isSecured() {
+ return false;
+ }
+
+ @Override
+ public String getURL() {
+ return StringUtils.removeEnd(StringUtils.defaultIfBlank(props.property("sonar.host.url"), "http://localhost:9000"), "/");
+ }
+
+ @Override
+ public String getPermanentServerId() {
+ return settings.getString(CoreProperties.PERMANENT_SERVER_ID);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/platform/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/platform/package-info.java
new file mode 100644
index 00000000000..cb2793702b0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/platform/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.platform;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/DefaultPostJobContext.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/DefaultPostJobContext.java
new file mode 100644
index 00000000000..02b1582b999
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/DefaultPostJobContext.java
@@ -0,0 +1,154 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.postjob;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+import javax.annotation.Nullable;
+
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.postjob.PostJobContext;
+import org.sonar.api.batch.postjob.issue.Issue;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.config.Settings;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.issue.IssueCache;
+
+public class DefaultPostJobContext implements PostJobContext {
+
+ private final Settings settings;
+ private final AnalysisMode analysisMode;
+ private final IssueCache cache;
+ private final BatchComponentCache resourceCache;
+
+ public DefaultPostJobContext(Settings settings, AnalysisMode analysisMode, IssueCache cache, BatchComponentCache resourceCache) {
+ this.settings = settings;
+ this.analysisMode = analysisMode;
+ this.cache = cache;
+ this.resourceCache = resourceCache;
+ }
+
+ @Override
+ public Settings settings() {
+ return settings;
+ }
+
+ @Override
+ public AnalysisMode analysisMode() {
+ return analysisMode;
+ }
+
+ @Override
+ public Iterable<Issue> issues() {
+ return Iterables.transform(Iterables.filter(cache.all(), new ResolvedPredicate(false)), new IssueTransformer());
+ }
+
+ @Override
+ public Iterable<Issue> resolvedIssues() {
+ return Iterables.transform(Iterables.filter(cache.all(), new ResolvedPredicate(true)), new IssueTransformer());
+ }
+
+ private class DefaultIssueWrapper implements Issue {
+
+ private final TrackedIssue wrapped;
+
+ public DefaultIssueWrapper(TrackedIssue wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public String key() {
+ return wrapped.key();
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return wrapped.getRuleKey();
+ }
+
+ @Override
+ public String componentKey() {
+ return wrapped.componentKey();
+ }
+
+ @Override
+ public InputComponent inputComponent() {
+ BatchComponent component = resourceCache.get(wrapped.componentKey());
+ return component != null ? component.inputComponent() : null;
+ }
+
+ @Override
+ public Integer line() {
+ return wrapped.startLine();
+ }
+
+ @Override
+ public Double effortToFix() {
+ return wrapped.gap();
+ }
+
+ @Override
+ public String message() {
+ return wrapped.getMessage();
+ }
+
+ @Override
+ public Severity severity() {
+ String severity = wrapped.severity();
+ return severity != null ? Severity.valueOf(severity) : null;
+ }
+
+ @Override
+ public boolean isNew() {
+ return wrapped.isNew();
+ }
+ }
+
+ private class IssueTransformer implements Function<TrackedIssue, Issue> {
+ @Override
+ public Issue apply(TrackedIssue input) {
+ return new DefaultIssueWrapper(input);
+ }
+ }
+
+ private static class ResolvedPredicate implements Predicate<TrackedIssue> {
+ private final boolean resolved;
+
+ private ResolvedPredicate(boolean resolved) {
+ this.resolved = resolved;
+ }
+
+ @Override
+ public boolean apply(@Nullable TrackedIssue issue) {
+ if (issue != null) {
+ return resolved ? issue.resolution() != null : issue.resolution() == null;
+ }
+ return false;
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobOptimizer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobOptimizer.java
new file mode 100644
index 00000000000..b27e89e87f8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobOptimizer.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.postjob;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.postjob.internal.DefaultPostJobDescriptor;
+import org.sonar.api.config.Settings;
+
+@BatchSide
+public class PostJobOptimizer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PostJobOptimizer.class);
+
+ private final Settings settings;
+ private final AnalysisMode analysisMode;
+
+ public PostJobOptimizer(Settings settings, AnalysisMode analysisMode) {
+ this.settings = settings;
+ this.analysisMode = analysisMode;
+ }
+
+ /**
+ * Decide if the given PostJob should be executed.
+ */
+ public boolean shouldExecute(DefaultPostJobDescriptor descriptor) {
+ if (!settingsCondition(descriptor)) {
+ LOG.debug("'{}' skipped because one of the required properties is missing", descriptor.name());
+ return false;
+ }
+ if (descriptor.isDisabledInIssues() && analysisMode.isIssues()) {
+ LOG.debug("'{}' skipped in issues mode", descriptor.name());
+ return false;
+ }
+ return true;
+ }
+
+ private boolean settingsCondition(DefaultPostJobDescriptor descriptor) {
+ if (!descriptor.properties().isEmpty()) {
+ for (String propertyKey : descriptor.properties()) {
+ if (!settings.hasKey(propertyKey)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobWrapper.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobWrapper.java
new file mode 100644
index 00000000000..c5ba661637c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/PostJobWrapper.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.postjob;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.CheckProject;
+import org.sonar.api.batch.postjob.PostJob;
+import org.sonar.api.batch.postjob.PostJobContext;
+import org.sonar.api.batch.postjob.internal.DefaultPostJobDescriptor;
+import org.sonar.api.resources.Project;
+
+public class PostJobWrapper implements org.sonar.api.batch.PostJob, CheckProject {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PostJobWrapper.class);
+
+ private PostJob wrappedPostJob;
+ private PostJobContext adaptor;
+ private DefaultPostJobDescriptor descriptor;
+ private PostJobOptimizer optimizer;
+
+ public PostJobWrapper(PostJob newPostJob, PostJobContext adaptor, PostJobOptimizer optimizer) {
+ this.wrappedPostJob = newPostJob;
+ this.optimizer = optimizer;
+ this.descriptor = new DefaultPostJobDescriptor();
+ newPostJob.describe(descriptor);
+ this.adaptor = adaptor;
+ }
+
+ public PostJob wrappedPostJob() {
+ return wrappedPostJob;
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return optimizer.shouldExecute(descriptor);
+ }
+
+ @Override
+ public void executeOn(Project project, org.sonar.api.batch.SensorContext context) {
+ wrappedPostJob.execute(adaptor);
+ }
+
+ @Override
+ public String toString() {
+ return descriptor.name() + (LOG.isDebugEnabled() ? " (wrapped)" : "");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/package-info.java
new file mode 100644
index 00000000000..0e8cb6ac72d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/postjob/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.batch.postjob;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/AbstractTimeProfiling.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/AbstractTimeProfiling.java
new file mode 100644
index 00000000000..6e204596616
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/AbstractTimeProfiling.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.profiling;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.TimeUtils;
+
+public abstract class AbstractTimeProfiling {
+
+ private final long startTime;
+
+ private long totalTime;
+
+ private System2 system;
+
+ public AbstractTimeProfiling(System2 system) {
+ this.system = system;
+ this.startTime = system.now();
+ }
+
+ protected System2 system() {
+ return system;
+ }
+
+ public long startTime() {
+ return startTime;
+ }
+
+ public void stop() {
+ this.totalTime = system.now() - startTime;
+ }
+
+ public long totalTime() {
+ return totalTime;
+ }
+
+ public String totalTimeAsString() {
+ return TimeUtils.formatDuration(totalTime);
+ }
+
+ public void setTotalTime(long totalTime) {
+ this.totalTime = totalTime;
+ }
+
+ protected void add(AbstractTimeProfiling other) {
+ this.setTotalTime(this.totalTime() + other.totalTime());
+ }
+
+ static <G extends AbstractTimeProfiling> Map<Object, G> sortByDescendingTotalTime(Map<?, G> unsorted) {
+ List<Map.Entry<?, G>> entries =
+ new ArrayList<Map.Entry<?, G>>(unsorted.entrySet());
+ Collections.sort(entries, new Comparator<Map.Entry<?, G>>() {
+ @Override
+ public int compare(Map.Entry<?, G> o1, Map.Entry<?, G> o2) {
+ return Long.valueOf(o2.getValue().totalTime()).compareTo(o1.getValue().totalTime());
+ }
+ });
+ Map<Object, G> sortedMap = new LinkedHashMap<>();
+ for (Map.Entry<?, G> entry : entries) {
+ sortedMap.put(entry.getKey(), entry.getValue());
+ }
+ return sortedMap;
+ }
+
+ static <G extends AbstractTimeProfiling> List<G> truncate(Collection<G> sortedList) {
+ int maxSize = 10;
+ List<G> result = new ArrayList<>(maxSize);
+ int i = 0;
+ for (G item : sortedList) {
+ if (i >= maxSize || item.totalTime() == 0) {
+ return result;
+ }
+ i++;
+ result.add(item);
+ }
+ return result;
+ }
+
+ protected void println(String msg) {
+ PhasesSumUpTimeProfiler.println(msg);
+ }
+
+ protected void println(String text, @Nullable Double percent, AbstractTimeProfiling phaseProfiling) {
+ PhasesSumUpTimeProfiler.println(text, percent, phaseProfiling);
+ }
+
+ protected void println(String text, AbstractTimeProfiling phaseProfiling) {
+ println(text, null, phaseProfiling);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ItemProfiling.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ItemProfiling.java
new file mode 100644
index 00000000000..abbb11ccbcb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ItemProfiling.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.profiling;
+
+import org.sonar.api.utils.System2;
+
+public class ItemProfiling extends AbstractTimeProfiling {
+
+ private final String itemName;
+
+ public ItemProfiling(System2 system, String itemName) {
+ super(system);
+ this.itemName = itemName;
+ }
+
+ public String itemName() {
+ return itemName;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ModuleProfiling.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ModuleProfiling.java
new file mode 100644
index 00000000000..7c04dcff55e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/ModuleProfiling.java
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.profiling;
+
+import com.google.common.collect.Maps;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import javax.annotation.Nullable;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.System2;
+
+public class ModuleProfiling extends AbstractTimeProfiling {
+
+ private Map<Phase, PhaseProfiling> profilingPerPhase = new HashMap<>();
+ private Map<String, ItemProfiling> profilingPerBatchStep = new LinkedHashMap<>();
+ private final Project module;
+
+ public ModuleProfiling(@Nullable Project module, System2 system) {
+ super(system);
+ this.module = module;
+ }
+
+ public String moduleName() {
+ if (module != null) {
+ return module.getName();
+ }
+ return null;
+ }
+
+ public PhaseProfiling getProfilingPerPhase(Phase phase) {
+ return profilingPerPhase.get(phase);
+ }
+
+ public ItemProfiling getProfilingPerBatchStep(String stepName) {
+ return profilingPerBatchStep.get(stepName);
+ }
+
+ public void addPhaseProfiling(Phase phase) {
+ profilingPerPhase.put(phase, PhaseProfiling.create(system(), phase));
+ }
+
+ public void addBatchStepProfiling(String stepName) {
+ profilingPerBatchStep.put(stepName, new ItemProfiling(system(), stepName));
+ }
+
+ public void dump(Properties props) {
+ double percent = this.totalTime() / 100.0;
+ Map<Object, AbstractTimeProfiling> categories = Maps.newLinkedHashMap();
+ categories.putAll(profilingPerPhase);
+ categories.putAll(profilingPerBatchStep);
+
+ for (Map.Entry<Object, AbstractTimeProfiling> batchStep : categories.entrySet()) {
+ props.setProperty(batchStep.getKey().toString(), Long.toString(batchStep.getValue().totalTime()));
+ }
+
+ for (Map.Entry<Object, AbstractTimeProfiling> batchStep : sortByDescendingTotalTime(categories).entrySet()) {
+ println(" * " + batchStep.getKey() + " execution time: ", percent, batchStep.getValue());
+ }
+ // Breakdown per phase
+ for (Phase phase : Phase.values()) {
+ if (profilingPerPhase.containsKey(phase) && getProfilingPerPhase(phase).hasItems()) {
+ println("");
+ println(" * " + phase + " execution time breakdown: ", getProfilingPerPhase(phase));
+ getProfilingPerPhase(phase).dump(props);
+ }
+ }
+ }
+
+ public void merge(ModuleProfiling other) {
+ super.add(other);
+ for (Entry<Phase, PhaseProfiling> entry : other.profilingPerPhase.entrySet()) {
+ if (!this.profilingPerPhase.containsKey(entry.getKey())) {
+ this.addPhaseProfiling(entry.getKey());
+ }
+ this.getProfilingPerPhase(entry.getKey()).merge(entry.getValue());
+ }
+ for (Map.Entry<String, ItemProfiling> entry : other.profilingPerBatchStep.entrySet()) {
+ if (!this.profilingPerBatchStep.containsKey(entry.getKey())) {
+ profilingPerBatchStep.put(entry.getKey(), new ItemProfiling(system(), entry.getKey()));
+ }
+ this.getProfilingPerBatchStep(entry.getKey()).add(entry.getValue());
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/Phase.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/Phase.java
new file mode 100644
index 00000000000..f00496f9644
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/Phase.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.profiling;
+
+public enum Phase {
+
+ INIT("Initializers"), SENSOR("Sensors"), DECORATOR("Decorators"), PERSISTER("Persisters"), POSTJOB("Post-Jobs");
+
+ private final String label;
+
+ private Phase(String label) {
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhaseProfiling.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhaseProfiling.java
new file mode 100644
index 00000000000..5b13f1d4ee1
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhaseProfiling.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.profiling;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import org.sonar.api.utils.System2;
+
+public class PhaseProfiling extends AbstractTimeProfiling {
+
+ private final Phase phase;
+
+ private Map<String, ItemProfiling> profilingPerItem = new HashMap<>();
+
+ PhaseProfiling(System2 system, Phase phase) {
+ super(system);
+ this.phase = phase;
+ }
+
+ public static PhaseProfiling create(System2 system, Phase phase) {
+ return new PhaseProfiling(system, phase);
+ }
+
+ public Phase phase() {
+ return phase;
+ }
+
+ public boolean hasItems() {
+ return !profilingPerItem.isEmpty();
+ }
+
+ public ItemProfiling getProfilingPerItem(Object item) {
+ String stringOrSimpleName = toStringOrSimpleName(item);
+ return profilingPerItem.get(stringOrSimpleName);
+ }
+
+ public void newItemProfiling(Object item) {
+ String stringOrSimpleName = toStringOrSimpleName(item);
+ profilingPerItem.put(stringOrSimpleName, new ItemProfiling(system(), stringOrSimpleName));
+ }
+
+ public void newItemProfiling(String itemName) {
+ profilingPerItem.put(itemName, new ItemProfiling(system(), itemName));
+ }
+
+ public void merge(PhaseProfiling other) {
+ super.add(other);
+ for (Entry<String, ItemProfiling> entry : other.profilingPerItem.entrySet()) {
+ if (!this.profilingPerItem.containsKey(entry.getKey())) {
+ newItemProfiling(entry.getKey());
+ }
+ this.getProfilingPerItem(entry.getKey()).add(entry.getValue());
+ }
+ }
+
+ public void dump(Properties props) {
+ double percent = this.totalTime() / 100.0;
+ for (ItemProfiling itemProfiling : profilingPerItem.values()) {
+ props.setProperty(itemProfiling.itemName(), Long.toString(itemProfiling.totalTime()));
+ }
+ for (ItemProfiling itemProfiling : truncate(sortByDescendingTotalTime(profilingPerItem).values())) {
+ println(" o " + itemProfiling.itemName() + ": ", percent, itemProfiling);
+ }
+ }
+
+ /**
+ * Try to use toString if it is not the default {@link Object#toString()}. Else use {@link Class#getSimpleName()}
+ * @param o
+ * @return
+ */
+ private static String toStringOrSimpleName(Object o) {
+ String toString = o.toString();
+ if (toString == null || toString.startsWith(o.getClass().getName())) {
+ return o.getClass().getSimpleName();
+ }
+ return toString;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhasesSumUpTimeProfiler.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhasesSumUpTimeProfiler.java
new file mode 100644
index 00000000000..f03b84caaac
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/PhasesSumUpTimeProfiler.java
@@ -0,0 +1,278 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.profiling;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.events.DecoratorExecutionHandler;
+import org.sonar.api.batch.events.DecoratorsPhaseHandler;
+import org.sonar.api.batch.events.InitializerExecutionHandler;
+import org.sonar.api.batch.events.InitializersPhaseHandler;
+import org.sonar.api.batch.events.PostJobExecutionHandler;
+import org.sonar.api.batch.events.PostJobsPhaseHandler;
+import org.sonar.api.batch.events.ProjectAnalysisHandler;
+import org.sonar.api.batch.events.SensorExecutionHandler;
+import org.sonar.api.batch.events.SensorsPhaseHandler;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.TimeUtils;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.events.BatchStepHandler;
+import org.sonar.batch.util.BatchUtils;
+
+import static org.sonar.batch.profiling.AbstractTimeProfiling.sortByDescendingTotalTime;
+import static org.sonar.batch.profiling.AbstractTimeProfiling.truncate;
+
+public class PhasesSumUpTimeProfiler implements ProjectAnalysisHandler, SensorExecutionHandler, DecoratorExecutionHandler, PostJobExecutionHandler, DecoratorsPhaseHandler,
+ SensorsPhaseHandler, PostJobsPhaseHandler, InitializersPhaseHandler, InitializerExecutionHandler, BatchStepHandler {
+
+ static final Logger LOG = LoggerFactory.getLogger(PhasesSumUpTimeProfiler.class);
+ private static final int TEXT_RIGHT_PAD = 60;
+ private static final int TIME_LEFT_PAD = 10;
+
+ @VisibleForTesting
+ ModuleProfiling currentModuleProfiling;
+
+ @VisibleForTesting
+ ModuleProfiling totalProfiling;
+
+ private Map<Project, ModuleProfiling> modulesProfilings = new HashMap<>();
+ private DecoratorsProfiler decoratorsProfiler;
+
+ private final System2 system;
+ private final File out;
+
+ public PhasesSumUpTimeProfiler(System2 system, GlobalProperties bootstrapProps) {
+ String workingDirPath = StringUtils.defaultIfBlank(bootstrapProps.property(CoreProperties.WORKING_DIRECTORY), CoreProperties.WORKING_DIRECTORY_DEFAULT_VALUE);
+ File workingDir = new File(workingDirPath).getAbsoluteFile();
+ this.out = new File(workingDir, "profiling");
+ this.out.mkdirs();
+ this.totalProfiling = new ModuleProfiling(null, system);
+ this.system = system;
+ }
+
+ static void println(String msg) {
+ LOG.info(msg);
+ }
+
+ static void println(String text, @Nullable Double percent, AbstractTimeProfiling phaseProfiling) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(StringUtils.rightPad(text, TEXT_RIGHT_PAD)).append(StringUtils.leftPad(phaseProfiling.totalTimeAsString(), TIME_LEFT_PAD));
+ if (percent != null) {
+ sb.append(" (").append((int) (phaseProfiling.totalTime() / percent)).append("%)");
+ }
+ println(sb.toString());
+ }
+
+ @Override
+ public void onProjectAnalysis(ProjectAnalysisEvent event) {
+ Project module = event.getProject();
+ if (event.isStart()) {
+ decoratorsProfiler = new DecoratorsProfiler();
+ currentModuleProfiling = new ModuleProfiling(module, system);
+ } else {
+ currentModuleProfiling.stop();
+ modulesProfilings.put(module, currentModuleProfiling);
+ long moduleTotalTime = currentModuleProfiling.totalTime();
+ println("");
+ println(" -------- Profiling of module " + module.getName() + ": " + TimeUtils.formatDuration(moduleTotalTime) + " --------");
+ println("");
+ Properties props = new Properties();
+ currentModuleProfiling.dump(props);
+ println("");
+ println(" -------- End of profiling of module " + module.getName() + " --------");
+ println("");
+ String fileName = module.getKey() + "-profiler.properties";
+ dumpToFile(props, BatchUtils.cleanKeyForFilename(fileName));
+ totalProfiling.merge(currentModuleProfiling);
+ if (module.isRoot() && !module.getModules().isEmpty()) {
+ dumpTotalExecutionSummary();
+ }
+ }
+ }
+
+ private void dumpTotalExecutionSummary() {
+ totalProfiling.stop();
+ long totalTime = totalProfiling.totalTime();
+ println("");
+ println(" ======== Profiling of total execution: " + TimeUtils.formatDuration(totalTime) + " ========");
+ println("");
+ println(" * Module execution time breakdown: ");
+ double percent = totalTime / 100.0;
+ for (ModuleProfiling modulesProfiling : truncate(sortByDescendingTotalTime(modulesProfilings).values())) {
+ println(" o " + modulesProfiling.moduleName() + " execution time: ", percent, modulesProfiling);
+ }
+ println("");
+ Properties props = new Properties();
+ totalProfiling.dump(props);
+ println("");
+ println(" ======== End of profiling of total execution ========");
+ println("");
+ String fileName = "total-execution-profiler.properties";
+ dumpToFile(props, fileName);
+ }
+
+ private void dumpToFile(Properties props, String fileName) {
+ File file = new File(out, fileName);
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ props.store(fos, "SonarQube");
+ println("Profiling data stored in " + file.getAbsolutePath());
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to store profiler output: " + file, e);
+ }
+ }
+
+ @Override
+ public void onSensorsPhase(SensorsPhaseEvent event) {
+ if (event.isStart()) {
+ currentModuleProfiling.addPhaseProfiling(Phase.SENSOR);
+ } else {
+ currentModuleProfiling.getProfilingPerPhase(Phase.SENSOR).stop();
+ }
+ }
+
+ @Override
+ public void onSensorExecution(SensorExecutionEvent event) {
+ PhaseProfiling profiling = currentModuleProfiling.getProfilingPerPhase(Phase.SENSOR);
+ if (event.isStart()) {
+ profiling.newItemProfiling(event.getSensor());
+ } else {
+ profiling.getProfilingPerItem(event.getSensor()).stop();
+ }
+ }
+
+ @Override
+ public void onDecoratorExecution(DecoratorExecutionEvent event) {
+ PhaseProfiling profiling = currentModuleProfiling.getProfilingPerPhase(Phase.DECORATOR);
+ if (event.isStart()) {
+ if (profiling.getProfilingPerItem(event.getDecorator()) == null) {
+ profiling.newItemProfiling(event.getDecorator());
+ }
+ decoratorsProfiler.start(event.getDecorator());
+ } else {
+ decoratorsProfiler.stop();
+ }
+ }
+
+ @Override
+ public void onDecoratorsPhase(DecoratorsPhaseEvent event) {
+ if (event.isStart()) {
+ currentModuleProfiling.addPhaseProfiling(Phase.DECORATOR);
+ } else {
+ for (Decorator decorator : decoratorsProfiler.getDurations().keySet()) {
+ currentModuleProfiling.getProfilingPerPhase(Phase.DECORATOR)
+ .getProfilingPerItem(decorator).setTotalTime(decoratorsProfiler.getDurations().get(decorator));
+ }
+ currentModuleProfiling.getProfilingPerPhase(Phase.DECORATOR).stop();
+ }
+ }
+
+ @Override
+ public void onPostJobsPhase(PostJobsPhaseEvent event) {
+ if (event.isStart()) {
+ currentModuleProfiling.addPhaseProfiling(Phase.POSTJOB);
+ } else {
+ currentModuleProfiling.getProfilingPerPhase(Phase.POSTJOB).stop();
+ }
+ }
+
+ @Override
+ public void onPostJobExecution(PostJobExecutionEvent event) {
+ PhaseProfiling profiling = currentModuleProfiling.getProfilingPerPhase(Phase.POSTJOB);
+ if (event.isStart()) {
+ profiling.newItemProfiling(event.getPostJob());
+ } else {
+ profiling.getProfilingPerItem(event.getPostJob()).stop();
+ }
+ }
+
+ @Override
+ public void onInitializersPhase(InitializersPhaseEvent event) {
+ if (event.isStart()) {
+ currentModuleProfiling.addPhaseProfiling(Phase.INIT);
+ } else {
+ currentModuleProfiling.getProfilingPerPhase(Phase.INIT).stop();
+ }
+ }
+
+ @Override
+ public void onInitializerExecution(InitializerExecutionEvent event) {
+ PhaseProfiling profiling = currentModuleProfiling.getProfilingPerPhase(Phase.INIT);
+ if (event.isStart()) {
+ profiling.newItemProfiling(event.getInitializer());
+ } else {
+ profiling.getProfilingPerItem(event.getInitializer()).stop();
+ }
+ }
+
+ @Override
+ public void onBatchStep(BatchStepEvent event) {
+ if (event.isStart()) {
+ currentModuleProfiling.addBatchStepProfiling(event.stepName());
+ } else {
+ currentModuleProfiling.getProfilingPerBatchStep(event.stepName()).stop();
+ }
+ }
+
+ class DecoratorsProfiler {
+ private List<Decorator> decorators = Lists.newArrayList();
+ private Map<Decorator, Long> durations = new IdentityHashMap<>();
+ private long startTime;
+ private Decorator currentDecorator;
+
+ DecoratorsProfiler() {
+ }
+
+ void start(Decorator decorator) {
+ this.startTime = system.now();
+ this.currentDecorator = decorator;
+ }
+
+ void stop() {
+ final Long cumulatedDuration;
+ if (durations.containsKey(currentDecorator)) {
+ cumulatedDuration = durations.get(currentDecorator);
+ } else {
+ decorators.add(currentDecorator);
+ cumulatedDuration = 0L;
+ }
+ durations.put(currentDecorator, cumulatedDuration + (system.now() - startTime));
+ }
+
+ public Map<Decorator, Long> getDurations() {
+ return durations;
+ }
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/package-info.java
new file mode 100644
index 00000000000..8f57dd4e4c7
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/profiling/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.profiling;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ActiveRulesPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ActiveRulesPublisher.java
new file mode 100644
index 00000000000..ba5dd00fbac
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ActiveRulesPublisher.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import org.sonar.api.batch.rule.ActiveRule;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.scanner.protocol.Constants;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+public class ActiveRulesPublisher implements ReportPublisherStep {
+
+ private final ActiveRules activeRules;
+
+ public ActiveRulesPublisher(ActiveRules activeRules) {
+ this.activeRules = activeRules;
+ }
+
+ @Override
+ public void publish(ScannerReportWriter writer) {
+ Iterable<ScannerReport.ActiveRule> activeRuleMessages = FluentIterable.from(activeRules.findAll()).transform(new ToMessage());
+ writer.writeActiveRules(activeRuleMessages);
+ }
+
+ private static class ToMessage implements Function<ActiveRule, ScannerReport.ActiveRule> {
+ private final ScannerReport.ActiveRule.Builder builder = ScannerReport.ActiveRule.newBuilder();
+
+ @Override
+ public ScannerReport.ActiveRule apply(@Nonnull ActiveRule input) {
+ builder.clear();
+ builder.setRuleRepository(input.ruleKey().repository());
+ builder.setRuleKey(input.ruleKey().rule());
+ builder.setSeverity(Constants.Severity.valueOf(input.severity()));
+ for (Map.Entry<String, String> entry : input.params().entrySet()) {
+ builder.addParamBuilder().setKey(entry.getKey()).setValue(entry.getValue()).build();
+
+ }
+ return builder.build();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/AnalysisContextReportPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/AnalysisContextReportPublisher.java
new file mode 100644
index 00000000000..614e80d71a4
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/AnalysisContextReportPublisher.java
@@ -0,0 +1,164 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeSet;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.bootstrap.BatchPluginRepository;
+import org.sonar.batch.repository.ProjectRepositories;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+@BatchSide
+public class AnalysisContextReportPublisher {
+
+ private static final Logger LOG = Loggers.get(AnalysisContextReportPublisher.class);
+
+ private static final String ENV_PROP_PREFIX = "env.";
+ private static final String SONAR_PROP_PREFIX = "sonar.";
+ private final BatchPluginRepository pluginRepo;
+ private final AnalysisMode mode;
+ private final System2 system;
+ private final ProjectRepositories projectRepos;
+
+ private ScannerReportWriter writer;
+
+ public AnalysisContextReportPublisher(AnalysisMode mode, BatchPluginRepository pluginRepo, System2 system, ProjectRepositories projectRepos) {
+ this.mode = mode;
+ this.pluginRepo = pluginRepo;
+ this.system = system;
+ this.projectRepos = projectRepos;
+ }
+
+ public void init(ScannerReportWriter writer) {
+ if (mode.isIssues()) {
+ return;
+ }
+ this.writer = writer;
+ File analysisLog = writer.getFileStructure().analysisLog();
+ try (BufferedWriter fileWriter = Files.newBufferedWriter(analysisLog.toPath(), StandardCharsets.UTF_8)) {
+ if (LOG.isDebugEnabled()) {
+ writeEnvVariables(fileWriter);
+ writeSystemProps(fileWriter);
+ }
+ writePlugins(fileWriter);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to write analysis log", e);
+ }
+ }
+
+ private void writePlugins(BufferedWriter fileWriter) throws IOException {
+ fileWriter.write("SonarQube plugins:\n");
+ for (PluginInfo p : pluginRepo.getPluginInfos()) {
+ fileWriter.append(String.format(" - %s %s (%s)", p.getName(), p.getVersion(), p.getKey())).append('\n');
+ }
+ }
+
+ private void writeSystemProps(BufferedWriter fileWriter) throws IOException {
+ fileWriter.write("System properties:\n");
+ Properties sysProps = system.properties();
+ for (String prop : new TreeSet<>(sysProps.stringPropertyNames())) {
+ if (prop.startsWith(SONAR_PROP_PREFIX)) {
+ continue;
+ }
+ fileWriter.append(String.format(" - %s=%s", prop, sysProps.getProperty(prop))).append('\n');
+ }
+ }
+
+ private void writeEnvVariables(BufferedWriter fileWriter) throws IOException {
+ fileWriter.append("Environment variables:\n");
+ Map<String, String> envVariables = system.envVariables();
+ for (String env : new TreeSet<>(envVariables.keySet())) {
+ fileWriter.append(String.format(" - %s=%s", env, envVariables.get(env))).append('\n');
+ }
+ }
+
+ public void dumpSettings(ProjectDefinition moduleDefinition) {
+ if (mode.isIssues()) {
+ return;
+ }
+
+ File analysisLog = writer.getFileStructure().analysisLog();
+ try (BufferedWriter fileWriter = Files.newBufferedWriter(analysisLog.toPath(), StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
+ Map<String, String> moduleSpecificProps = collectModuleSpecificProps(moduleDefinition);
+ fileWriter.append(String.format("Settings for module: %s", moduleDefinition.getKey())).append('\n');
+ for (String prop : new TreeSet<>(moduleSpecificProps.keySet())) {
+ if (isSystemProp(prop) || isEnvVariable(prop) || !isSqProp(prop)) {
+ continue;
+ }
+ fileWriter.append(String.format(" - %s=%s", prop, sensitive(prop) ? "******" : moduleSpecificProps.get(prop))).append('\n');
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to write analysis log", e);
+ }
+ }
+
+ /**
+ * Only keep props that are not in parent
+ */
+ private Map<String, String> collectModuleSpecificProps(ProjectDefinition moduleDefinition) {
+ Map<String, String> moduleSpecificProps = new HashMap<>();
+ if (projectRepos.moduleExists(moduleDefinition.getKeyWithBranch())) {
+ moduleSpecificProps.putAll(projectRepos.settings(moduleDefinition.getKeyWithBranch()));
+ }
+ ProjectDefinition parent = moduleDefinition.getParent();
+ if (parent == null) {
+ moduleSpecificProps.putAll(moduleDefinition.properties());
+ } else {
+ Map<String, String> parentProps = parent.properties();
+ for (Map.Entry<String, String> entry : moduleDefinition.properties().entrySet()) {
+ if (!parentProps.containsKey(entry.getKey()) || !parentProps.get(entry.getKey()).equals(entry.getValue())) {
+ moduleSpecificProps.put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ return moduleSpecificProps;
+ }
+
+ private static boolean isSqProp(String propKey) {
+ return propKey.startsWith(SONAR_PROP_PREFIX);
+ }
+
+ private boolean isSystemProp(String propKey) {
+ return system.properties().containsKey(propKey) && !propKey.startsWith(SONAR_PROP_PREFIX);
+ }
+
+ private boolean isEnvVariable(String propKey) {
+ return propKey.startsWith(ENV_PROP_PREFIX) && system.envVariables().containsKey(propKey.substring(ENV_PROP_PREFIX.length()));
+ }
+
+ private static boolean sensitive(String key) {
+ return key.contains(".password") || key.contains(".secured");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ComponentsPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ComponentsPublisher.java
new file mode 100644
index 00000000000..0aa66d0bdca
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ComponentsPublisher.java
@@ -0,0 +1,175 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import javax.annotation.CheckForNull;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+import org.sonar.scanner.protocol.Constants;
+import org.sonar.scanner.protocol.Constants.ComponentLinkType;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.protocol.output.ScannerReport.ComponentLink;
+
+/**
+ * Adds components and analysis metadata to output report
+ */
+public class ComponentsPublisher implements ReportPublisherStep {
+
+ private final BatchComponentCache resourceCache;
+ private final ImmutableProjectReactor reactor;
+
+ public ComponentsPublisher(ImmutableProjectReactor reactor, BatchComponentCache resourceCache) {
+ this.reactor = reactor;
+ this.resourceCache = resourceCache;
+ }
+
+ @Override
+ public void publish(ScannerReportWriter writer) {
+ BatchComponent rootProject = resourceCache.get(reactor.getRoot().getKeyWithBranch());
+ recursiveWriteComponent(rootProject, writer);
+ }
+
+ private void recursiveWriteComponent(BatchComponent batchComponent, ScannerReportWriter writer) {
+ Resource r = batchComponent.resource();
+ ScannerReport.Component.Builder builder = ScannerReport.Component.newBuilder();
+
+ // non-null fields
+ builder.setRef(batchComponent.batchId());
+ builder.setType(getType(r));
+
+ // Don't set key on directories and files to save space since it can be deduced from path
+ if (batchComponent.isProjectOrModule()) {
+ // Here we want key without branch
+ ProjectDefinition def = reactor.getProjectDefinition(batchComponent.key());
+ builder.setKey(def.getKey());
+ }
+
+ // protocol buffers does not accept null values
+
+ if (batchComponent.isFile()) {
+ builder.setIsTest(ResourceUtils.isUnitTestFile(r));
+ builder.setLines(((InputFile) batchComponent.inputComponent()).lines());
+ }
+ String name = getName(r);
+ if (name != null) {
+ builder.setName(name);
+ }
+ String description = getDescription(r);
+ if (description != null) {
+ builder.setDescription(description);
+ }
+ String path = r.getPath();
+ if (path != null) {
+ builder.setPath(path);
+ }
+ String lang = getLanguageKey(r);
+ if (lang != null) {
+ builder.setLanguage(lang);
+ }
+ for (BatchComponent child : batchComponent.children()) {
+ builder.addChildRef(child.batchId());
+ }
+ writeLinks(batchComponent, builder);
+ writeVersion(batchComponent, builder);
+ writer.writeComponent(builder.build());
+
+ for (BatchComponent child : batchComponent.children()) {
+ recursiveWriteComponent(child, writer);
+ }
+ }
+
+ private void writeVersion(BatchComponent c, ScannerReport.Component.Builder builder) {
+ if (c.isProjectOrModule()) {
+ ProjectDefinition def = reactor.getProjectDefinition(c.key());
+ String version = getVersion(def);
+ builder.setVersion(version);
+ }
+ }
+
+ private static String getVersion(ProjectDefinition def) {
+ String version = def.getVersion();
+ return StringUtils.isNotBlank(version) ? version : getVersion(def.getParent());
+ }
+
+ private void writeLinks(BatchComponent c, ScannerReport.Component.Builder builder) {
+ if (c.isProjectOrModule()) {
+ ProjectDefinition def = reactor.getProjectDefinition(c.key());
+ ComponentLink.Builder linkBuilder = ComponentLink.newBuilder();
+
+ writeProjectLink(builder, def, linkBuilder, CoreProperties.LINKS_HOME_PAGE, ComponentLinkType.HOME);
+ writeProjectLink(builder, def, linkBuilder, CoreProperties.LINKS_CI, ComponentLinkType.CI);
+ writeProjectLink(builder, def, linkBuilder, CoreProperties.LINKS_ISSUE_TRACKER, ComponentLinkType.ISSUE);
+ writeProjectLink(builder, def, linkBuilder, CoreProperties.LINKS_SOURCES, ComponentLinkType.SCM);
+ writeProjectLink(builder, def, linkBuilder, CoreProperties.LINKS_SOURCES_DEV, ComponentLinkType.SCM_DEV);
+ }
+ }
+
+ private static void writeProjectLink(ScannerReport.Component.Builder componentBuilder, ProjectDefinition def, ComponentLink.Builder linkBuilder, String linkProp,
+ ComponentLinkType linkType) {
+ String link = def.properties().get(linkProp);
+ if (StringUtils.isNotBlank(link)) {
+ linkBuilder.setType(linkType);
+ linkBuilder.setHref(link);
+ componentBuilder.addLink(linkBuilder.build());
+ linkBuilder.clear();
+ }
+ }
+
+ @CheckForNull
+ private static String getLanguageKey(Resource r) {
+ Language language = r.getLanguage();
+ return ResourceUtils.isFile(r) && language != null ? language.getKey() : null;
+ }
+
+ @CheckForNull
+ private static String getName(Resource r) {
+ // Don't return name for directories and files since it can be guessed from the path
+ return (ResourceUtils.isFile(r) || ResourceUtils.isDirectory(r)) ? null : r.getName();
+ }
+
+ @CheckForNull
+ private static String getDescription(Resource r) {
+ // Only for projets and modules
+ return ResourceUtils.isProject(r) ? r.getDescription() : null;
+ }
+
+ private Constants.ComponentType getType(Resource r) {
+ if (ResourceUtils.isFile(r)) {
+ return Constants.ComponentType.FILE;
+ } else if (ResourceUtils.isDirectory(r)) {
+ return Constants.ComponentType.DIRECTORY;
+ } else if (ResourceUtils.isModuleProject(r)) {
+ return Constants.ComponentType.MODULE;
+ } else if (ResourceUtils.isRootProject(r)) {
+ return Constants.ComponentType.PROJECT;
+ }
+ throw new IllegalArgumentException("Unknown resource type: " + r);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/CoveragePublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/CoveragePublisher.java
new file mode 100644
index 00000000000..1d6b9db71ef
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/CoveragePublisher.java
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.protocol.output.ScannerReport.Coverage;
+import org.sonar.scanner.protocol.output.ScannerReport.Coverage.Builder;
+
+public class CoveragePublisher implements ReportPublisherStep {
+
+ private final BatchComponentCache resourceCache;
+ private final MeasureCache measureCache;
+
+ public CoveragePublisher(BatchComponentCache resourceCache, MeasureCache measureCache) {
+ this.resourceCache = resourceCache;
+ this.measureCache = measureCache;
+ }
+
+ @Override
+ public void publish(ScannerReportWriter writer) {
+ for (final BatchComponent resource : resourceCache.all()) {
+ if (!resource.isFile()) {
+ continue;
+ }
+ Map<Integer, Coverage.Builder> coveragePerLine = new LinkedHashMap<>();
+
+ int lineCount = ((InputFile) resource.inputComponent()).lines();
+ applyLineMeasure(resource.key(), lineCount, CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY, coveragePerLine,
+ new MeasureOperation() {
+ @Override
+ public void apply(String value, Coverage.Builder builder) {
+ builder.setUtHits(Integer.parseInt(value) > 0);
+ }
+ });
+ applyLineMeasure(resource.key(), lineCount, CoreMetrics.CONDITIONS_BY_LINE_KEY, coveragePerLine,
+ new MeasureOperation() {
+ @Override
+ public void apply(String value, Coverage.Builder builder) {
+ builder.setConditions(Integer.parseInt(value));
+ }
+ });
+ applyLineMeasure(resource.key(), lineCount, CoreMetrics.COVERED_CONDITIONS_BY_LINE_KEY, coveragePerLine,
+ new MeasureOperation() {
+ @Override
+ public void apply(String value, Coverage.Builder builder) {
+ builder.setUtCoveredConditions(Integer.parseInt(value));
+ }
+ });
+ applyLineMeasure(resource.key(), lineCount, CoreMetrics.IT_COVERAGE_LINE_HITS_DATA_KEY, coveragePerLine,
+ new MeasureOperation() {
+ @Override
+ public void apply(String value, Coverage.Builder builder) {
+ builder.setItHits(Integer.parseInt(value) > 0);
+ }
+ });
+ applyLineMeasure(resource.key(), lineCount, CoreMetrics.IT_COVERED_CONDITIONS_BY_LINE_KEY, coveragePerLine,
+ new MeasureOperation() {
+ @Override
+ public void apply(String value, Coverage.Builder builder) {
+ builder.setItCoveredConditions(Integer.parseInt(value));
+ }
+ });
+ applyLineMeasure(resource.key(), lineCount, CoreMetrics.OVERALL_COVERED_CONDITIONS_BY_LINE_KEY, coveragePerLine,
+ new MeasureOperation() {
+ @Override
+ public void apply(String value, Coverage.Builder builder) {
+ builder.setOverallCoveredConditions(Integer.parseInt(value));
+ }
+ });
+ writer.writeComponentCoverage(resource.batchId(), Iterables.transform(coveragePerLine.values(), BuildCoverage.INSTANCE));
+ }
+ }
+
+ void applyLineMeasure(String inputFileKey, int lineCount, String metricKey, Map<Integer, Coverage.Builder> coveragePerLine, MeasureOperation op) {
+ Measure measure = measureCache.byMetric(inputFileKey, metricKey);
+ if (measure != null) {
+ Map<Integer, String> lineMeasures = KeyValueFormat.parseIntString((String) measure.value());
+ for (Map.Entry<Integer, String> lineMeasure : lineMeasures.entrySet()) {
+ int lineIdx = lineMeasure.getKey();
+ if (lineIdx <= lineCount) {
+ String value = lineMeasure.getValue();
+ if (StringUtils.isNotEmpty(value)) {
+ Coverage.Builder coverageBuilder = coveragePerLine.get(lineIdx);
+ if (coverageBuilder == null) {
+ coverageBuilder = Coverage.newBuilder();
+ coverageBuilder.setLine(lineIdx);
+ coveragePerLine.put(lineIdx, coverageBuilder);
+ }
+ op.apply(value, coverageBuilder);
+ }
+ }
+ }
+ }
+ }
+
+ interface MeasureOperation {
+ void apply(String value, Coverage.Builder builder);
+ }
+
+ private enum BuildCoverage implements Function<Coverage.Builder, Coverage> {
+ INSTANCE;
+
+ @Override
+ public Coverage apply(@Nonnull Builder input) {
+ return input.build();
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/MeasuresPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/MeasuresPublisher.java
new file mode 100644
index 00000000000..51e8867ddca
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/MeasuresPublisher.java
@@ -0,0 +1,168 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import java.io.Serializable;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import org.sonar.api.batch.measure.Metric;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric.ValueType;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.core.metric.BatchMetrics;
+import org.sonar.scanner.protocol.Constants.MeasureValueType;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.collect.Sets.newHashSet;
+
+public class MeasuresPublisher implements ReportPublisherStep {
+
+ private static final class MeasureToReportMeasure implements Function<Measure, ScannerReport.Measure> {
+ private final BatchComponent resource;
+ private final ScannerReport.Measure.Builder builder = ScannerReport.Measure.newBuilder();
+
+ private MeasureToReportMeasure(BatchComponent resource) {
+ this.resource = resource;
+ }
+
+ @Override
+ public ScannerReport.Measure apply(@Nonnull Measure input) {
+ validateMeasure(input, resource.key());
+ return toReportMeasure(builder, input);
+ }
+
+ private static void validateMeasure(Measure measure, String componentKey) {
+ if (measure.getValue() == null && measure.getData() == null) {
+ throw new IllegalArgumentException(String.format("Measure on metric '%s' and component '%s' has no value, but it's not allowed", measure.getMetricKey(), componentKey));
+ }
+ }
+
+ private ScannerReport.Measure toReportMeasure(ScannerReport.Measure.Builder builder, Measure measure) {
+ builder.clear();
+
+ builder.setValueType(getMeasureValueType(measure.getMetric().getType()));
+ setValueAccordingToType(builder, measure);
+ // Because some numeric measures also have a data (like maintainability rating)
+ String data = measure.getData();
+ if (data != null) {
+ builder.setStringValue(data);
+ }
+ builder.setMetricKey(measure.getMetricKey());
+ return builder.build();
+ }
+
+ private void setValueAccordingToType(ScannerReport.Measure.Builder builder, Measure measure) {
+ Serializable value = measure.value();
+ switch (builder.getValueType()) {
+ case BOOLEAN:
+ builder.setBooleanValue((Boolean) value);
+ break;
+ case DOUBLE:
+ builder.setDoubleValue(((Number) value).doubleValue());
+ break;
+ case INT:
+ builder.setIntValue(((Number) value).intValue());
+ break;
+ case LONG:
+ builder.setLongValue(((Number) value).longValue());
+ break;
+ case STRING:
+ builder.setStringValue((String) value);
+ break;
+ default:
+ throw new IllegalStateException("Unknown value type: " + builder.getValueType());
+ }
+ }
+
+ private MeasureValueType getMeasureValueType(ValueType type) {
+ switch (type) {
+ case INT:
+ case RATING:
+ return MeasureValueType.INT;
+ case FLOAT:
+ case PERCENT:
+ return MeasureValueType.DOUBLE;
+ case BOOL:
+ return MeasureValueType.BOOLEAN;
+ case STRING:
+ case DATA:
+ case LEVEL:
+ case DISTRIB:
+ return MeasureValueType.STRING;
+ case WORK_DUR:
+ case MILLISEC:
+ return MeasureValueType.LONG;
+ default:
+ throw new IllegalStateException("Unknown value type: " + type);
+ }
+ }
+
+ }
+
+ private static final class IsMetricAllowed implements Predicate<Measure> {
+ private final Set<String> allowedMetricKeys;
+
+ private IsMetricAllowed(Set<String> allowedMetricKeys) {
+ this.allowedMetricKeys = allowedMetricKeys;
+ }
+
+ @Override
+ public boolean apply(Measure input) {
+ return allowedMetricKeys.contains(input.getMetricKey());
+ }
+ }
+
+ private static final class MetricToKey implements Function<Metric, String> {
+ @Override
+ public String apply(Metric input) {
+ return input.key();
+ }
+ }
+
+ private final BatchComponentCache resourceCache;
+ private final MeasureCache measureCache;
+ private final BatchMetrics batchMetrics;
+
+ public MeasuresPublisher(BatchComponentCache resourceCache, MeasureCache measureCache, BatchMetrics batchMetrics) {
+ this.resourceCache = resourceCache;
+ this.measureCache = measureCache;
+ this.batchMetrics = batchMetrics;
+ }
+
+ @Override
+ public void publish(ScannerReportWriter writer) {
+ final Set<String> allowedMetricKeys = newHashSet(transform(batchMetrics.getMetrics(), new MetricToKey()));
+ for (final BatchComponent resource : resourceCache.all()) {
+ Iterable<Measure> batchMeasures = measureCache.byResource(resource.resource());
+ Iterable<org.sonar.scanner.protocol.output.ScannerReport.Measure> reportMeasures = transform(
+ filter(batchMeasures, new IsMetricAllowed(allowedMetricKeys)),
+ new MeasureToReportMeasure(resource));
+ writer.writeComponentMeasures(resource.batchId(), reportMeasures);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/MetadataPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/MetadataPublisher.java
new file mode 100644
index 00000000000..67dc6c0be04
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/MetadataPublisher.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+public class MetadataPublisher implements ReportPublisherStep {
+
+ private final BatchComponentCache componentCache;
+ private final ImmutableProjectReactor reactor;
+ private final Settings settings;
+
+ public MetadataPublisher(BatchComponentCache componentCache, ImmutableProjectReactor reactor, Settings settings) {
+ this.componentCache = componentCache;
+ this.reactor = reactor;
+ this.settings = settings;
+ }
+
+ @Override
+ public void publish(ScannerReportWriter writer) {
+ ProjectDefinition root = reactor.getRoot();
+ BatchComponent rootProject = componentCache.getRoot();
+ ScannerReport.Metadata.Builder builder = ScannerReport.Metadata.newBuilder()
+ .setAnalysisDate(((Project) rootProject.resource()).getAnalysisDate().getTime())
+ // Here we want key without branch
+ .setProjectKey(root.getKey())
+ .setCrossProjectDuplicationActivated(SonarCpdBlockIndex.isCrossProjectDuplicationEnabled(settings))
+ .setRootComponentRef(rootProject.batchId());
+ String branch = root.properties().get(CoreProperties.PROJECT_BRANCH_PROPERTY);
+ if (branch != null) {
+ builder.setBranch(branch);
+ }
+ writer.writeMetadata(builder.build());
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisher.java
new file mode 100644
index 00000000000..c0487441c10
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisher.java
@@ -0,0 +1,242 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.io.Files;
+import com.squareup.okhttp.HttpUrl;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.picocontainer.Startable;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.TempFolder;
+import org.sonar.api.utils.ZipUtils;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.WsCe;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static org.apache.commons.lang.StringUtils.trimToEmpty;
+import static org.sonar.core.util.FileUtils.deleteQuietly;
+
+@BatchSide
+public class ReportPublisher implements Startable {
+
+ private static final Logger LOG = Loggers.get(ReportPublisher.class);
+
+ public static final String KEEP_REPORT_PROP_KEY = "sonar.batch.keepReport";
+ public static final String VERBOSE_KEY = "sonar.verbose";
+ public static final String METADATA_DUMP_FILENAME = "report-task.txt";
+
+ private final Settings settings;
+ private final BatchWsClient wsClient;
+ private final AnalysisContextReportPublisher contextPublisher;
+ private final ImmutableProjectReactor projectReactor;
+ private final DefaultAnalysisMode analysisMode;
+ private final TempFolder temp;
+ private final ReportPublisherStep[] publishers;
+
+ private File reportDir;
+ private ScannerReportWriter writer;
+
+ public ReportPublisher(Settings settings, BatchWsClient wsClient, AnalysisContextReportPublisher contextPublisher,
+ ImmutableProjectReactor projectReactor, DefaultAnalysisMode analysisMode, TempFolder temp, ReportPublisherStep[] publishers) {
+ this.settings = settings;
+ this.wsClient = wsClient;
+ this.contextPublisher = contextPublisher;
+ this.projectReactor = projectReactor;
+ this.analysisMode = analysisMode;
+ this.temp = temp;
+ this.publishers = publishers;
+ }
+
+ @Override
+ public void start() {
+ reportDir = new File(projectReactor.getRoot().getWorkDir(), "batch-report");
+ writer = new ScannerReportWriter(reportDir);
+ contextPublisher.init(writer);
+
+ if (!analysisMode.isIssues() && !analysisMode.isMediumTest()) {
+ String publicUrl = publicUrl();
+ if (HttpUrl.parse(publicUrl) == null) {
+ throw MessageException.of("Failed to parse public URL set in SonarQube server: " + publicUrl);
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (!settings.getBoolean(KEEP_REPORT_PROP_KEY) && !settings.getBoolean(VERBOSE_KEY)) {
+ deleteQuietly(reportDir);
+ } else {
+ LOG.info("Analysis report generated in " + reportDir);
+ }
+ }
+
+ public File getReportDir() {
+ return reportDir;
+ }
+
+ public ScannerReportWriter getWriter() {
+ return writer;
+ }
+
+ public void execute() {
+ // If this is a issues mode analysis then we should not upload reports
+ String taskId = null;
+ if (!analysisMode.isIssues()) {
+ File report = generateReportFile();
+ if (!analysisMode.isMediumTest()) {
+ taskId = upload(report);
+ }
+ }
+ logSuccess(taskId);
+ }
+
+ private File generateReportFile() {
+ try {
+ long startTime = System.currentTimeMillis();
+ for (ReportPublisherStep publisher : publishers) {
+ publisher.publish(writer);
+ }
+ long stopTime = System.currentTimeMillis();
+ LOG.info("Analysis report generated in {}ms, dir size={}", stopTime - startTime, FileUtils.byteCountToDisplaySize(FileUtils.sizeOfDirectory(reportDir)));
+
+ startTime = System.currentTimeMillis();
+ File reportZip = temp.newFile("batch-report", ".zip");
+ ZipUtils.zipDir(reportDir, reportZip);
+ stopTime = System.currentTimeMillis();
+ LOG.info("Analysis reports compressed in {}ms, zip size={}", stopTime - startTime, FileUtils.byteCountToDisplaySize(FileUtils.sizeOf(reportZip)));
+ return reportZip;
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to prepare analysis report", e);
+ }
+ }
+
+ /**
+ * Uploads the report file to server and returns the generated task id
+ */
+ @VisibleForTesting
+ String upload(File report) {
+ LOG.debug("Upload report");
+ long startTime = System.currentTimeMillis();
+ ProjectDefinition projectDefinition = projectReactor.getRoot();
+ PostRequest.Part filePart = new PostRequest.Part(MediaTypes.ZIP, report);
+ PostRequest post = new PostRequest("api/ce/submit")
+ .setMediaType(MediaTypes.PROTOBUF)
+ .setParam("projectKey", projectDefinition.getKey())
+ .setParam("projectName", projectDefinition.getName())
+ .setParam("projectBranch", projectDefinition.getBranch())
+ .setPart("report", filePart);
+ WsResponse response = wsClient.call(post).failIfNotSuccessful();
+ try (InputStream protobuf = response.contentStream()) {
+ return WsCe.SubmitResponse.parser().parseFrom(protobuf).getTaskId();
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ } finally {
+ long stopTime = System.currentTimeMillis();
+ LOG.info("Analysis report uploaded in " + (stopTime - startTime) + "ms");
+ }
+ }
+
+ @VisibleForTesting
+ void logSuccess(@Nullable String taskId) {
+ if (taskId == null) {
+ LOG.info("ANALYSIS SUCCESSFUL");
+ } else {
+ String publicUrl = publicUrl();
+ HttpUrl httpUrl = HttpUrl.parse(publicUrl);
+
+ Map<String, String> metadata = new LinkedHashMap<>();
+ String effectiveKey = projectReactor.getRoot().getKeyWithBranch();
+ metadata.put("projectKey", effectiveKey);
+ metadata.put("serverUrl", publicUrl);
+
+ URL dashboardUrl = httpUrl.newBuilder()
+ .addPathSegment("dashboard").addPathSegment("index").addPathSegment(effectiveKey)
+ .build()
+ .url();
+ metadata.put("dashboardUrl", dashboardUrl.toExternalForm());
+
+ URL taskUrl = HttpUrl.parse(publicUrl).newBuilder()
+ .addPathSegment("api").addPathSegment("ce").addPathSegment("task")
+ .addQueryParameter("id", taskId)
+ .build()
+ .url();
+ metadata.put("ceTaskId", taskId);
+ metadata.put("ceTaskUrl", taskUrl.toExternalForm());
+
+ LOG.info("ANALYSIS SUCCESSFUL, you can browse {}", dashboardUrl);
+ LOG.info("Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report");
+ LOG.info("More about the report processing at {}", taskUrl);
+
+ dumpMetadata(metadata);
+ }
+ }
+
+ private void dumpMetadata(Map<String, String> metadata) {
+ File file = new File(projectReactor.getRoot().getWorkDir(), METADATA_DUMP_FILENAME);
+ try (Writer output = Files.newWriter(file, StandardCharsets.UTF_8)) {
+ for (Map.Entry<String, String> entry : metadata.entrySet()) {
+ output.write(entry.getKey());
+ output.write("=");
+ output.write(entry.getValue());
+ output.write("\n");
+ }
+
+ LOG.debug("Report metadata written to {}", file);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to dump " + file, e);
+ }
+ }
+
+ /**
+ * The public URL is optionally configured on server. If not, then the regular URL is returned.
+ * See https://jira.sonarsource.com/browse/SONAR-4239
+ */
+ private String publicUrl() {
+ String baseUrl = trimToEmpty(settings.getString(CoreProperties.SERVER_BASE_URL));
+ if (baseUrl.equals(settings.getDefaultValue(CoreProperties.SERVER_BASE_URL))) {
+ // crap workaround for https://jira.sonarsource.com/browse/SONAR-7109
+ // If server base URL was not configured in Sonar server then is is better to take URL configured on batch side
+ baseUrl = wsClient.baseUrl();
+ }
+ return baseUrl.replaceAll("(/)+$", "");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisherStep.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisherStep.java
new file mode 100644
index 00000000000..8edcacb4b48
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ReportPublisherStep.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+/**
+ * Adds a sub-part of data to output report
+ */
+public interface ReportPublisherStep {
+
+ void publish(ScannerReportWriter writer);
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ScannerReportUtils.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ScannerReportUtils.java
new file mode 100644
index 00000000000..b5e026476d3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/ScannerReportUtils.java
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.scanner.protocol.Constants.HighlightingType;
+
+public class ScannerReportUtils {
+
+ private ScannerReportUtils() {
+ }
+
+ public static HighlightingType toProtocolType(TypeOfText textType) {
+ switch (textType) {
+ case ANNOTATION:
+ return HighlightingType.ANNOTATION;
+ case COMMENT:
+ return HighlightingType.COMMENT;
+ case CONSTANT:
+ return HighlightingType.CONSTANT;
+ case CPP_DOC:
+ return HighlightingType.CPP_DOC;
+ case KEYWORD:
+ return HighlightingType.KEYWORD;
+ case KEYWORD_LIGHT:
+ return HighlightingType.KEYWORD_LIGHT;
+ case PREPROCESS_DIRECTIVE:
+ return HighlightingType.PREPROCESS_DIRECTIVE;
+ case STRING:
+ return HighlightingType.HIGHLIGHTING_STRING;
+ case STRUCTURED_COMMENT:
+ return HighlightingType.STRUCTURED_COMMENT;
+ default:
+ throw new IllegalArgumentException("Unknow highlighting type: " + textType);
+ }
+ }
+
+ public static TypeOfText toBatchType(HighlightingType type) {
+ switch (type) {
+ case ANNOTATION:
+ return TypeOfText.ANNOTATION;
+ case COMMENT:
+ return TypeOfText.COMMENT;
+ case CONSTANT:
+ return TypeOfText.CONSTANT;
+ case CPP_DOC:
+ return TypeOfText.CPP_DOC;
+ case HIGHLIGHTING_STRING:
+ return TypeOfText.STRING;
+ case KEYWORD:
+ return TypeOfText.KEYWORD;
+ case KEYWORD_LIGHT:
+ return TypeOfText.KEYWORD_LIGHT;
+ case PREPROCESS_DIRECTIVE:
+ return TypeOfText.PREPROCESS_DIRECTIVE;
+ case STRUCTURED_COMMENT:
+ return TypeOfText.STRUCTURED_COMMENT;
+ default:
+ throw new IllegalArgumentException(type + " is not a valid type");
+ }
+ }
+
+ public static String toCssClass(HighlightingType type) {
+ return toBatchType(type).cssClass();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/SourcePublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/SourcePublisher.java
new file mode 100644
index 00000000000..b6b5a1d1745
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/SourcePublisher.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import org.apache.commons.io.ByteOrderMark;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.BOMInputStream;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+public class SourcePublisher implements ReportPublisherStep {
+
+ private final BatchComponentCache resourceCache;
+
+ public SourcePublisher(BatchComponentCache resourceCache) {
+ this.resourceCache = resourceCache;
+ }
+
+ @Override
+ public void publish(ScannerReportWriter writer) {
+ for (final BatchComponent resource : resourceCache.all()) {
+ if (!resource.isFile()) {
+ continue;
+ }
+
+ DefaultInputFile inputFile = (DefaultInputFile) resource.inputComponent();
+ File iofile = writer.getSourceFile(resource.batchId());
+ int line = 0;
+ try (FileOutputStream output = new FileOutputStream(iofile); BOMInputStream bomIn = new BOMInputStream(new FileInputStream(inputFile.file()),
+ ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(bomIn, inputFile.charset()))) {
+ String lineStr = reader.readLine();
+ while (lineStr != null) {
+ IOUtils.write(lineStr, output, StandardCharsets.UTF_8);
+ line++;
+ if (line < inputFile.lines()) {
+ IOUtils.write("\n", output, StandardCharsets.UTF_8);
+ }
+ lineStr = reader.readLine();
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to store file source in the report", e);
+ }
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/TestExecutionAndCoveragePublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/TestExecutionAndCoveragePublisher.java
new file mode 100644
index 00000000000..7561eb1ed1d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/TestExecutionAndCoveragePublisher.java
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import java.util.HashSet;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.test.CoverageBlock;
+import org.sonar.api.test.MutableTestCase;
+import org.sonar.api.test.MutableTestPlan;
+import org.sonar.api.test.TestCase;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.test.DefaultTestable;
+import org.sonar.batch.test.TestPlanBuilder;
+import org.sonar.scanner.protocol.Constants.TestStatus;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.protocol.output.ScannerReport.CoverageDetail;
+import org.sonar.scanner.protocol.output.ScannerReport.Test;
+
+public class TestExecutionAndCoveragePublisher implements ReportPublisherStep {
+
+ private static final class TestConverter implements Function<MutableTestCase, ScannerReport.Test> {
+ private final Set<String> testNamesWithCoverage;
+ private ScannerReport.Test.Builder builder = ScannerReport.Test.newBuilder();
+
+ private TestConverter(Set<String> testNamesWithCoverage) {
+ this.testNamesWithCoverage = testNamesWithCoverage;
+ }
+
+ @Override
+ public Test apply(@Nonnull MutableTestCase testCase) {
+ builder.clear();
+ builder.setName(testCase.name());
+ if (testCase.doesCover()) {
+ testNamesWithCoverage.add(testCase.name());
+ }
+ Long durationInMs = testCase.durationInMs();
+ if (durationInMs != null) {
+ builder.setDurationInMs(durationInMs);
+ }
+ String msg = testCase.message();
+ if (msg != null) {
+ builder.setMsg(msg);
+ }
+ String stack = testCase.stackTrace();
+ if (stack != null) {
+ builder.setStacktrace(stack);
+ }
+ TestCase.Status status = testCase.status();
+ if (status != null) {
+ builder.setStatus(TestStatus.valueOf(status.name()));
+ }
+ return builder.build();
+ }
+ }
+
+ private final class TestCoverageConverter implements Function<String, CoverageDetail> {
+ private final MutableTestPlan testPlan;
+ private ScannerReport.CoverageDetail.Builder builder = ScannerReport.CoverageDetail.newBuilder();
+ private ScannerReport.CoverageDetail.CoveredFile.Builder coveredBuilder = ScannerReport.CoverageDetail.CoveredFile.newBuilder();
+
+ private TestCoverageConverter(MutableTestPlan testPlan) {
+ this.testPlan = testPlan;
+ }
+
+ @Override
+ public CoverageDetail apply(@Nonnull String testName) {
+ // Take first test with provided name
+ MutableTestCase testCase = testPlan.testCasesByName(testName).iterator().next();
+ builder.clear();
+ builder.setTestName(testName);
+ for (CoverageBlock block : testCase.coverageBlocks()) {
+ coveredBuilder.clear();
+ coveredBuilder.setFileRef(componentCache.get(((DefaultTestable) block.testable()).inputFile().key()).batchId());
+ for (int line : block.lines()) {
+ coveredBuilder.addCoveredLine(line);
+ }
+ builder.addCoveredFile(coveredBuilder.build());
+ }
+ return builder.build();
+ }
+ }
+
+ private final BatchComponentCache componentCache;
+ private final TestPlanBuilder testPlanBuilder;
+
+ public TestExecutionAndCoveragePublisher(BatchComponentCache resourceCache, TestPlanBuilder testPlanBuilder) {
+ this.componentCache = resourceCache;
+ this.testPlanBuilder = testPlanBuilder;
+ }
+
+ @Override
+ public void publish(ScannerReportWriter writer) {
+ for (final BatchComponent component : componentCache.all()) {
+ if (!component.isFile()) {
+ continue;
+ }
+
+ DefaultInputFile inputFile = (DefaultInputFile) component.inputComponent();
+ if (inputFile.type() != Type.TEST) {
+ continue;
+ }
+
+ final MutableTestPlan testPlan = testPlanBuilder.loadPerspective(MutableTestPlan.class, component);
+ if (testPlan == null || Iterables.isEmpty(testPlan.testCases())) {
+ continue;
+ }
+
+ final Set<String> testNamesWithCoverage = new HashSet<>();
+
+ writer.writeTests(component.batchId(), Iterables.transform(testPlan.testCases(), new TestConverter(testNamesWithCoverage)));
+
+ writer.writeCoverageDetails(component.batchId(), Iterables.transform(testNamesWithCoverage, new TestCoverageConverter(testPlan)));
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/report/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/package-info.java
new file mode 100644
index 00000000000..f4b3d8e51a9
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/report/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.report;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoader.java
new file mode 100644
index 00000000000..55508b0e8bf
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoader.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+import org.sonar.batch.cache.WSLoader;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+public class DefaultGlobalRepositoriesLoader implements GlobalRepositoriesLoader {
+
+ private static final String BATCH_GLOBAL_URL = "/batch/global";
+
+ private final WSLoader wsLoader;
+
+ public DefaultGlobalRepositoriesLoader(WSLoader wsLoader) {
+ this.wsLoader = wsLoader;
+ }
+
+ @Override
+ public GlobalRepositories load(@Nullable MutableBoolean fromCache) {
+ WSLoaderResult<String> result = wsLoader.loadString(BATCH_GLOBAL_URL);
+ if (fromCache != null) {
+ fromCache.setValue(result.isFromCache());
+ }
+ return GlobalRepositories.fromJson(result.get());
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoader.java
new file mode 100644
index 00000000000..0e8a0904b57
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoader.java
@@ -0,0 +1,129 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import com.google.common.base.Throwables;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.Date;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.util.BatchUtils;
+import org.sonarqube.ws.WsBatch.WsProjectResponse;
+import org.sonarqube.ws.WsBatch.WsProjectResponse.FileDataByPath;
+import org.sonarqube.ws.WsBatch.WsProjectResponse.Settings;
+import org.sonarqube.ws.client.HttpException;
+
+public class DefaultProjectRepositoriesLoader implements ProjectRepositoriesLoader {
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultProjectRepositoriesLoader.class);
+ private static final String BATCH_PROJECT_URL = "/batch/project.protobuf";
+ private final WSLoader loader;
+
+ public DefaultProjectRepositoriesLoader(WSLoader loader) {
+ this.loader = loader;
+ }
+
+ @Override
+ public ProjectRepositories load(String projectKey, boolean issuesMode, @Nullable MutableBoolean fromCache) {
+ try {
+ WSLoaderResult<InputStream> result = loader.loadStream(getUrl(projectKey, issuesMode));
+ if (fromCache != null) {
+ fromCache.setValue(result.isFromCache());
+ }
+ return processStream(result.get(), projectKey);
+ } catch (RuntimeException e) {
+ if (shouldThrow(e)) {
+ throw e;
+ }
+
+ LOG.debug("Project repository not available - continuing without it", e);
+ return new ProjectRepositories();
+ }
+ }
+
+ private static String getUrl(String projectKey, boolean issuesMode) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(BATCH_PROJECT_URL)
+ .append("?key=").append(BatchUtils.encodeForUrl(projectKey));
+ if (issuesMode) {
+ builder.append("&issues_mode=true");
+ }
+ return builder.toString();
+ }
+
+ private static boolean shouldThrow(Exception e) {
+ for (Throwable t : Throwables.getCausalChain(e)) {
+ if (t instanceof HttpException) {
+ HttpException http = (HttpException) t;
+ return http.code() != HttpURLConnection.HTTP_NOT_FOUND;
+ }
+ if (t instanceof MessageException) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static ProjectRepositories processStream(InputStream is, String projectKey) {
+ try {
+ WsProjectResponse response = WsProjectResponse.parseFrom(is);
+
+ Table<String, String, FileData> fileDataTable = HashBasedTable.create();
+ Table<String, String, String> settings = HashBasedTable.create();
+
+ Map<String, Settings> settingsByModule = response.getSettingsByModule();
+ for (Map.Entry<String, Settings> e1 : settingsByModule.entrySet()) {
+ for (Map.Entry<String, String> e2 : e1.getValue().getSettings().entrySet()) {
+ settings.put(e1.getKey(), e2.getKey(), e2.getValue());
+ }
+ }
+
+ Map<String, FileDataByPath> fileDataByModuleAndPath = response.getFileDataByModuleAndPath();
+ for (Map.Entry<String, FileDataByPath> e1 : fileDataByModuleAndPath.entrySet()) {
+ for (Map.Entry<String, org.sonarqube.ws.WsBatch.WsProjectResponse.FileData> e2 : e1.getValue().getFileDataByPath().entrySet()) {
+ FileData fd = new FileData(e2.getValue().getHash(), e2.getValue().getRevision());
+ fileDataTable.put(e1.getKey(), e2.getKey(), fd);
+ }
+ }
+
+ return new ProjectRepositories(settings, fileDataTable, new Date(response.getLastAnalysisDate()));
+ } catch (IOException e) {
+ throw new IllegalStateException("Couldn't load project repository for " + projectKey, e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultQualityProfileLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultQualityProfileLoader.java
new file mode 100644
index 00000000000..524e6f6856d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultQualityProfileLoader.java
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.sonar.api.utils.MessageException;
+
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse;
+import org.sonar.batch.util.BatchUtils;
+import org.apache.commons.io.IOUtils;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+public class DefaultQualityProfileLoader implements QualityProfileLoader {
+ private static final String WS_URL = "/api/qualityprofiles/search.protobuf";
+
+ private WSLoader wsLoader;
+
+ public DefaultQualityProfileLoader(WSLoader wsLoader) {
+ this.wsLoader = wsLoader;
+ }
+
+ @Override
+ public List<QualityProfile> loadDefault(@Nullable String profileName, @Nullable MutableBoolean fromCache) {
+ String url = WS_URL + "?defaults=true";
+ if (profileName != null) {
+ url += "&profileName=" + BatchUtils.encodeForUrl(profileName);
+ }
+ return loadResource(url, fromCache);
+ }
+
+ @Override
+ public List<QualityProfile> load(String projectKey, @Nullable String profileName, @Nullable MutableBoolean fromCache) {
+ String url = WS_URL + "?projectKey=" + BatchUtils.encodeForUrl(projectKey);
+ if (profileName != null) {
+ url += "&profileName=" + BatchUtils.encodeForUrl(profileName);
+ }
+ return loadResource(url, fromCache);
+ }
+
+ private List<QualityProfile> loadResource(String url, @Nullable MutableBoolean fromCache) {
+ WSLoaderResult<InputStream> result = wsLoader.loadStream(url);
+ if (fromCache != null) {
+ fromCache.setValue(result.isFromCache());
+ }
+ InputStream is = result.get();
+ SearchWsResponse profiles = null;
+
+ try {
+ profiles = SearchWsResponse.parseFrom(is);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to load quality profiles", e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+
+ List<QualityProfile> profilesList = profiles.getProfilesList();
+ if (profilesList == null || profilesList.isEmpty()) {
+ throw MessageException.of("No quality profiles have been found, you probably don't have any language plugin installed.");
+ }
+ return profilesList;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java
new file mode 100644
index 00000000000..394ce0eea92
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import com.google.common.base.Function;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.commons.io.IOUtils;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.util.BatchUtils;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+
+public class DefaultServerIssuesLoader implements ServerIssuesLoader {
+
+ private final WSLoader wsLoader;
+
+ public DefaultServerIssuesLoader(WSLoader wsLoader) {
+ this.wsLoader = wsLoader;
+ }
+
+ @Override
+ public boolean load(String componentKey, Function<ServerIssue, Void> consumer) {
+ WSLoaderResult<InputStream> result = wsLoader.loadStream("/batch/issues.protobuf?key=" + BatchUtils.encodeForUrl(componentKey));
+ parseIssues(result.get(), consumer);
+ return result.isFromCache();
+ }
+
+ private static void parseIssues(InputStream is, Function<ServerIssue, Void> consumer) {
+ try {
+ ServerIssue previousIssue = ServerIssue.parseDelimitedFrom(is);
+ while (previousIssue != null) {
+ consumer.apply(previousIssue);
+ previousIssue = ServerIssue.parseDelimitedFrom(is);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to get previous issues", e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/FileData.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/FileData.java
new file mode 100644
index 00000000000..bcf470e8986
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/FileData.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public class FileData {
+ private final String hash;
+ private final String revision;
+
+ public FileData(String hash, String revision) {
+ this.hash = hash;
+ this.revision = revision;
+ }
+
+ public String hash() {
+ return hash;
+ }
+
+ public String revision() {
+ return revision;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesLoader.java
new file mode 100644
index 00000000000..86359e4b772
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesLoader.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+import javax.annotation.Nullable;
+
+public interface GlobalRepositoriesLoader {
+
+ GlobalRepositories load(@Nullable MutableBoolean fromCache);
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesProvider.java
new file mode 100644
index 00000000000..578b5458cd8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/GlobalRepositoriesProvider.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+
+public class GlobalRepositoriesProvider extends ProviderAdapter {
+
+ private static final Logger LOG = Loggers.get(GlobalRepositoriesProvider.class);
+ private static final String LOG_MSG = "Load global repositories";
+ private GlobalRepositories globalReferentials;
+
+ public GlobalRepositories provide(GlobalRepositoriesLoader loader) {
+ if (globalReferentials == null) {
+ Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+ MutableBoolean fromCache = new MutableBoolean();
+ globalReferentials = loader.load(fromCache);
+ profiler.stopInfo(fromCache.booleanValue());
+ }
+ return globalReferentials;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositories.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositories.java
new file mode 100644
index 00000000000..baba0c52396
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositories.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import java.util.Date;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class ProjectRepositories {
+ private final Table<String, String, String> settingsByModule;
+ private final Table<String, String, FileData> fileDataByModuleAndPath;
+ private final Date lastAnalysisDate;
+ private final boolean exists;
+
+ public ProjectRepositories() {
+ this.exists = false;
+ this.settingsByModule = HashBasedTable.create();
+ this.fileDataByModuleAndPath = HashBasedTable.create();
+ this.lastAnalysisDate = null;
+ }
+
+ public ProjectRepositories(Table<String, String, String> settingsByModule, Table<String, String, FileData> fileDataByModuleAndPath,
+ @Nullable Date lastAnalysisDate) {
+ this.settingsByModule = settingsByModule;
+ this.fileDataByModuleAndPath = fileDataByModuleAndPath;
+ this.lastAnalysisDate = lastAnalysisDate;
+ this.exists = true;
+ }
+
+ public boolean exists() {
+ return exists;
+ }
+
+ public Map<String, FileData> fileDataByPath(String moduleKey) {
+ return fileDataByModuleAndPath.row(moduleKey);
+ }
+
+ public Table<String, String, FileData> fileDataByModuleAndPath() {
+ return fileDataByModuleAndPath;
+ }
+
+ public boolean moduleExists(String moduleKey) {
+ return settingsByModule.containsRow(moduleKey);
+ }
+
+ public Map<String, String> settings(String moduleKey) {
+ return settingsByModule.row(moduleKey);
+ }
+
+ @CheckForNull
+ public FileData fileData(String projectKey, String path) {
+ return fileDataByModuleAndPath.get(projectKey, path);
+ }
+
+ @CheckForNull
+ public Date lastAnalysisDate() {
+ return lastAnalysisDate;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesLoader.java
new file mode 100644
index 00000000000..53de4805324
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesLoader.java
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+public interface ProjectRepositoriesLoader {
+ ProjectRepositories load(String projectKeyWithBranch, boolean issuesMode, @Nullable MutableBoolean fromCache);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesProvider.java
new file mode 100644
index 00000000000..1e0469651d5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ProjectRepositoriesProvider.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.sonar.api.utils.log.Profiler;
+
+import org.sonar.api.batch.bootstrap.ProjectKey;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.picocontainer.injectors.ProviderAdapter;
+
+public class ProjectRepositoriesProvider extends ProviderAdapter {
+ private static final Logger LOG = Loggers.get(ProjectRepositoriesProvider.class);
+ private static final String LOG_MSG = "Load project repositories";
+ private ProjectRepositories project = null;
+
+ public ProjectRepositories provide(ProjectRepositoriesLoader loader, ProjectKey projectKey, DefaultAnalysisMode mode) {
+ if (project == null) {
+ MutableBoolean fromCache = new MutableBoolean(false);
+ Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+ if (mode.isNotAssociated()) {
+ project = createNonAssociatedProjectRepositories();
+ profiler.stopInfo();
+ } else {
+ project = loader.load(projectKey.get(), mode.isIssues(), fromCache);
+ checkProject(mode);
+ profiler.stopInfo(fromCache.booleanValue());
+ }
+
+ }
+
+ return project;
+ }
+
+ private void checkProject(DefaultAnalysisMode mode) {
+ if (mode.isIssues()) {
+ if (!project.exists()) {
+ LOG.warn("Project doesn't exist on the server. All issues will be marked as 'new'.");
+ } else if (project.lastAnalysisDate() == null && !mode.isNotAssociated()) {
+ LOG.warn("No analysis has been found on the server for this project. All issues will be marked as 'new'.");
+ }
+ }
+ }
+
+ private static ProjectRepositories createNonAssociatedProjectRepositories() {
+ return new ProjectRepositories();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileLoader.java
new file mode 100644
index 00000000000..a93bea66b11
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileLoader.java
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+public interface QualityProfileLoader {
+ List<QualityProfile> load(String projectKey, @Nullable String profileName, @Nullable MutableBoolean fromCache);
+
+ List<QualityProfile> loadDefault(@Nullable String profileName, @Nullable MutableBoolean fromCache);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileProvider.java
new file mode 100644
index 00000000000..6878d21ecfb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/QualityProfileProvider.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import java.util.List;
+import javax.annotation.CheckForNull;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.batch.bootstrap.ProjectKey;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.batch.analysis.AnalysisProperties;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.rule.ModuleQProfiles;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+public class QualityProfileProvider extends ProviderAdapter {
+ private static final Logger LOG = Loggers.get(QualityProfileProvider.class);
+ private static final String LOG_MSG = "Load quality profiles";
+ private ModuleQProfiles profiles = null;
+
+ public ModuleQProfiles provide(ProjectKey projectKey, QualityProfileLoader loader, ProjectRepositories projectRepositories, AnalysisProperties props, DefaultAnalysisMode mode) {
+ if (this.profiles == null) {
+ List<QualityProfile> profileList;
+ MutableBoolean fromCache = new MutableBoolean();
+
+ Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+ if (mode.isNotAssociated() || !projectRepositories.exists()) {
+ profileList = loader.loadDefault(getSonarProfile(props, mode), fromCache);
+ } else {
+ profileList = loader.load(projectKey.get(), getSonarProfile(props, mode), fromCache);
+ }
+ profiler.stopInfo(fromCache.booleanValue());
+ profiles = new ModuleQProfiles(profileList);
+ }
+
+ return profiles;
+ }
+
+ @CheckForNull
+ private static String getSonarProfile(AnalysisProperties props, DefaultAnalysisMode mode) {
+ String profile = null;
+ if (!mode.isIssues() && props.properties().containsKey(ModuleQProfiles.SONAR_PROFILE_PROP)) {
+ profile = props.property(ModuleQProfiles.SONAR_PROFILE_PROP);
+ LOG.warn("Ability to set quality profile from command line using '" + ModuleQProfiles.SONAR_PROFILE_PROP
+ + "' is deprecated and will be dropped in a future SonarQube version. Please configure quality profile used by your project on SonarQube server.");
+ }
+ return profile;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ServerIssuesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ServerIssuesLoader.java
new file mode 100644
index 00000000000..e0c981a11e0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/ServerIssuesLoader.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import com.google.common.base.Function;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+
+public interface ServerIssuesLoader {
+
+ boolean load(String componentKey, Function<ServerIssue, Void> consumer);
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/DefaultLanguagesRepository.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/DefaultLanguagesRepository.java
new file mode 100644
index 00000000000..4f891e64f3d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/DefaultLanguagesRepository.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository.language;
+
+import org.picocontainer.Startable;
+
+import org.sonar.api.resources.Languages;
+
+import javax.annotation.CheckForNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Languages repository using {@link Languages}
+ * @since 4.4
+ */
+public class DefaultLanguagesRepository implements LanguagesRepository, Startable {
+
+ private Languages languages;
+
+ public DefaultLanguagesRepository(Languages languages) {
+ this.languages = languages;
+ }
+
+ @Override
+ public void start() {
+ if (languages.all().length == 0) {
+ throw new IllegalStateException("No language plugins are installed.");
+ }
+ }
+
+ /**
+ * Get language.
+ */
+ @Override
+ @CheckForNull
+ public Language get(String languageKey) {
+ org.sonar.api.resources.Language language = languages.get(languageKey);
+ return language != null ? new Language(language.getKey(), language.getName(), language.getFileSuffixes()) : null;
+ }
+
+ /**
+ * Get list of all supported languages.
+ */
+ @Override
+ public Collection<Language> all() {
+ org.sonar.api.resources.Language[] all = languages.all();
+ Collection<Language> result = new ArrayList<>(all.length);
+ for (org.sonar.api.resources.Language language : all) {
+ result.add(new Language(language.getKey(), language.getName(), language.getFileSuffixes()));
+ }
+ return result;
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/Language.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/Language.java
new file mode 100644
index 00000000000..1a9ae783536
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/Language.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository.language;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+public final class Language {
+
+ private final String key;
+ private final String name;
+ private final String[] fileSuffixes;
+
+ public Language(String key, String name, String... fileSuffixes) {
+ this.key = key;
+ this.name = name;
+ this.fileSuffixes = fileSuffixes;
+ }
+
+ /**
+ * For example "java".
+ */
+ public String key() {
+ return key;
+ }
+
+ /**
+ * For example "Java"
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * For example ["jav", "java"].
+ */
+ public Collection<String> fileSuffixes() {
+ return Arrays.asList(fileSuffixes);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/LanguagesRepository.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/LanguagesRepository.java
new file mode 100644
index 00000000000..da1bab7993f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/LanguagesRepository.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository.language;
+
+import org.sonar.api.batch.BatchSide;
+
+import javax.annotation.CheckForNull;
+
+import java.util.Collection;
+
+/**
+ * Languages repository
+ * @since 4.4
+ */
+@BatchSide
+public interface LanguagesRepository {
+
+ /**
+ * Get language.
+ */
+ @CheckForNull
+ Language get(String languageKey);
+
+ /**
+ * Get list of all supported languages.
+ */
+ Collection<Language> all();
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/package-info.java
new file mode 100644
index 00000000000..03b2bf5f676
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/language/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.batch.repository.language;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/package-info.java
new file mode 100644
index 00000000000..3679d1bfbb1
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.repository;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java
new file mode 100644
index 00000000000..9b33645cebe
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java
@@ -0,0 +1,117 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository.user;
+
+import org.apache.commons.io.IOUtils;
+
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+import com.google.common.collect.Lists;
+import com.google.common.base.Joiner;
+import org.sonar.batch.util.BatchUtils;
+import org.sonar.scanner.protocol.input.ScannerInput;
+import com.google.common.base.Function;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class UserRepositoryLoader {
+ private final WSLoader wsLoader;
+
+ public UserRepositoryLoader(WSLoader wsLoader) {
+ this.wsLoader = wsLoader;
+ }
+
+ public ScannerInput.User load(String userLogin) {
+ return load(userLogin, null);
+ }
+
+ public ScannerInput.User load(String userLogin, @Nullable MutableBoolean fromCache) {
+ InputStream is = loadQuery(new UserEncodingFunction().apply(userLogin), fromCache);
+ return parseUser(is);
+ }
+
+ public Collection<ScannerInput.User> load(List<String> userLogins) {
+ return load(userLogins, null);
+ }
+
+ /**
+ * Not cache friendly. Should not be used if a cache hit is expected.
+ */
+ public Collection<ScannerInput.User> load(List<String> userLogins, @Nullable MutableBoolean fromCache) {
+ if (userLogins.isEmpty()) {
+ return Collections.emptyList();
+ }
+ InputStream is = loadQuery(Joiner.on(',').join(Lists.transform(userLogins, new UserEncodingFunction())), fromCache);
+
+ return parseUsers(is);
+ }
+
+ private InputStream loadQuery(String loginsQuery, @Nullable MutableBoolean fromCache) {
+ WSLoaderResult<InputStream> result = wsLoader.loadStream("/batch/users?logins=" + loginsQuery);
+ if (fromCache != null) {
+ fromCache.setValue(result.isFromCache());
+ }
+ return result.get();
+ }
+
+ private static class UserEncodingFunction implements Function<String, String> {
+ @Override
+ public String apply(String input) {
+ return BatchUtils.encodeForUrl(input);
+ }
+ }
+
+ private static ScannerInput.User parseUser(InputStream is) {
+ try {
+ return ScannerInput.User.parseDelimitedFrom(is);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to get user details from server", e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ }
+
+ private static Collection<ScannerInput.User> parseUsers(InputStream is) {
+ List<ScannerInput.User> users = new ArrayList<>();
+
+ try {
+ ScannerInput.User user = ScannerInput.User.parseDelimitedFrom(is);
+ while (user != null) {
+ users.add(user);
+ user = ScannerInput.User.parseDelimitedFrom(is);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to get user details from server", e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+
+ return users;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/package-info.java
new file mode 100644
index 00000000000..6462fb00b8a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/repository/user/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.repository.user;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesLoader.java
new file mode 100644
index 00000000000..028929bd141
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesLoader.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+public interface ActiveRulesLoader {
+ List<LoadedActiveRule> load(String qualityProfileKey, @Nullable MutableBoolean fromCache);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java
new file mode 100644
index 00000000000..83034e22132
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.batch.rule.internal.NewActiveRule;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+
+/**
+ * Loads the rules that are activated on the Quality profiles
+ * used by the current project and builds {@link org.sonar.api.batch.rule.ActiveRules}.
+ */
+public class ActiveRulesProvider extends ProviderAdapter {
+ private static final Logger LOG = Loggers.get(ActiveRulesProvider.class);
+ private static final String LOG_MSG = "Load active rules";
+ private ActiveRules singleton = null;
+
+ public ActiveRules provide(ActiveRulesLoader loader, ModuleQProfiles qProfiles) {
+ if (singleton == null) {
+ Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+ MutableBoolean fromCache = new MutableBoolean();
+ singleton = load(loader, qProfiles, fromCache);
+ profiler.stopInfo(fromCache.booleanValue());
+ }
+ return singleton;
+ }
+
+ private static ActiveRules load(ActiveRulesLoader loader, ModuleQProfiles qProfiles, MutableBoolean fromCache) {
+
+ Collection<String> qProfileKeys = getKeys(qProfiles);
+ Map<RuleKey, LoadedActiveRule> loadedRulesByKey = new HashMap<>();
+
+ try {
+ for (String qProfileKey : qProfileKeys) {
+ Collection<LoadedActiveRule> qProfileRules;
+ qProfileRules = load(loader, qProfileKey, fromCache);
+
+ for (LoadedActiveRule r : qProfileRules) {
+ if (!loadedRulesByKey.containsKey(r.getRuleKey())) {
+ loadedRulesByKey.put(r.getRuleKey(), r);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Error loading active rules", e);
+ }
+
+ return transform(loadedRulesByKey.values());
+ }
+
+ private static ActiveRules transform(Collection<LoadedActiveRule> loadedRules) {
+ ActiveRulesBuilder builder = new ActiveRulesBuilder();
+
+ for (LoadedActiveRule activeRule : loadedRules) {
+ NewActiveRule newActiveRule = builder.create(activeRule.getRuleKey());
+ newActiveRule.setName(activeRule.getName());
+ newActiveRule.setSeverity(activeRule.getSeverity());
+ newActiveRule.setLanguage(activeRule.getLanguage());
+ newActiveRule.setInternalKey(activeRule.getInternalKey());
+ newActiveRule.setTemplateRuleKey(activeRule.getTemplateRuleKey());
+
+ // load parameters
+ if (activeRule.getParams() != null) {
+ for (Map.Entry<String, String> params : activeRule.getParams().entrySet()) {
+ newActiveRule.setParam(params.getKey(), params.getValue());
+ }
+ }
+
+ newActiveRule.activate();
+ }
+ return builder.build();
+ }
+
+ private static List<LoadedActiveRule> load(ActiveRulesLoader loader, String qProfileKey, MutableBoolean fromCache) throws IOException {
+ return loader.load(qProfileKey, fromCache);
+ }
+
+ private static Collection<String> getKeys(ModuleQProfiles qProfiles) {
+ List<String> keys = new ArrayList<>(qProfiles.findAll().size());
+
+ for (QProfile qp : qProfiles.findAll()) {
+ keys.add(qp.getKey());
+ }
+
+ return keys;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultActiveRulesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultActiveRulesLoader.java
new file mode 100644
index 00000000000..9485d99e1cb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultActiveRulesLoader.java
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.sonar.batch.util.BatchUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonarqube.ws.Rules.Active;
+import org.sonarqube.ws.Rules.Active.Param;
+import org.sonarqube.ws.Rules.ActiveList;
+import org.sonarqube.ws.Rules.Rule;
+import org.sonarqube.ws.Rules.SearchResponse;
+
+public class DefaultActiveRulesLoader implements ActiveRulesLoader {
+ private static final String RULES_SEARCH_URL = "/api/rules/search.protobuf?f=repo,name,severity,lang,internalKey,templateKey,params,actives&activation=true";
+
+ private final WSLoader wsLoader;
+
+ public DefaultActiveRulesLoader(WSLoader wsLoader) {
+ this.wsLoader = wsLoader;
+ }
+
+ @Override
+ public List<LoadedActiveRule> load(String qualityProfileKey, @Nullable MutableBoolean fromCache) {
+ List<LoadedActiveRule> ruleList = new LinkedList<>();
+ int page = 1;
+ int pageSize = 500;
+ int loaded = 0;
+
+ while (true) {
+ WSLoaderResult<InputStream> result = wsLoader.loadStream(getUrl(qualityProfileKey, page, pageSize));
+ SearchResponse response = loadFromStream(result.get());
+ List<LoadedActiveRule> pageRules = readPage(response);
+ ruleList.addAll(pageRules);
+ loaded += response.getPs();
+
+ if (response.getTotal() <= loaded) {
+ break;
+ }
+ page++;
+ if (fromCache != null) {
+ fromCache.setValue(result.isFromCache());
+ }
+ }
+
+ return ruleList;
+ }
+
+ private static String getUrl(String qualityProfileKey, int page, int pageSize) {
+ StringBuilder builder = new StringBuilder(1024);
+ builder.append(RULES_SEARCH_URL);
+ builder.append("&qprofile=").append(BatchUtils.encodeForUrl(qualityProfileKey));
+ builder.append("&p=").append(page);
+ builder.append("&ps=").append(pageSize);
+ return builder.toString();
+ }
+
+ private static SearchResponse loadFromStream(InputStream is) {
+ try {
+ return SearchResponse.parseFrom(is);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to load quality profiles", e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ }
+
+ private static List<LoadedActiveRule> readPage(SearchResponse response) {
+ List<LoadedActiveRule> loadedRules = new LinkedList<>();
+
+ List<Rule> rulesList = response.getRulesList();
+ Map<String, ActiveList> actives = response.getActives().getActives();
+
+ for (Rule r : rulesList) {
+ ActiveList activeList = actives.get(r.getKey());
+ Active active = activeList.getActiveList(0);
+
+ LoadedActiveRule loadedRule = new LoadedActiveRule();
+
+ loadedRule.setRuleKey(RuleKey.parse(r.getKey()));
+ loadedRule.setName(r.getName());
+ loadedRule.setSeverity(active.getSeverity());
+ loadedRule.setLanguage(r.getLang());
+ loadedRule.setInternalKey(r.getInternalKey());
+ if (r.hasTemplateKey()) {
+ RuleKey templateRuleKey = RuleKey.parse(r.getTemplateKey());
+ loadedRule.setTemplateRuleKey(templateRuleKey.rule());
+ }
+
+ Map<String, String> params = new HashMap<>();
+
+ for (org.sonarqube.ws.Rules.Rule.Param param : r.getParams().getParamsList()) {
+ params.put(param.getKey(), param.getDefaultValue());
+ }
+
+ // overrides defaultValue if the key is the same
+ for (Param param : active.getParamsList()) {
+ params.put(param.getKey(), param.getValue());
+ }
+ loadedRule.setParams(params);
+ loadedRules.add(loadedRule);
+ }
+
+ return loadedRules;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java
new file mode 100644
index 00000000000..62292b4581b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.apache.commons.io.IOUtils;
+
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonarqube.ws.Rules.ListResponse.Rule;
+import org.sonarqube.ws.Rules.ListResponse;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+public class DefaultRulesLoader implements RulesLoader {
+ private static final String RULES_SEARCH_URL = "/api/rules/list.protobuf";
+
+ private final WSLoader wsLoader;
+
+ public DefaultRulesLoader(WSLoader wsLoader) {
+ this.wsLoader = wsLoader;
+ }
+
+ @Override
+ public List<Rule> load(@Nullable MutableBoolean fromCache) {
+ WSLoaderResult<InputStream> result = wsLoader.loadStream(RULES_SEARCH_URL);
+ ListResponse list = loadFromStream(result.get());
+ if (fromCache != null) {
+ fromCache.setValue(result.isFromCache());
+ }
+ return list.getRulesList();
+ }
+
+ private static ListResponse loadFromStream(InputStream is) {
+ try {
+ return ListResponse.parseFrom(is);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to get rules", e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/LoadedActiveRule.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/LoadedActiveRule.java
new file mode 100644
index 00000000000..31e5919220f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/LoadedActiveRule.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.rule.RuleKey;
+
+public class LoadedActiveRule {
+ private RuleKey ruleKey;
+ private String severity;
+ private String name;
+ private String language;
+ private Map<String, String> params;
+ private String templateRuleKey;
+ private String internalKey;
+
+ public RuleKey getRuleKey() {
+ return ruleKey;
+ }
+
+ public void setRuleKey(RuleKey ruleKey) {
+ this.ruleKey = ruleKey;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getSeverity() {
+ return severity;
+ }
+
+ public void setSeverity(String severity) {
+ this.severity = severity;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public Map<String, String> getParams() {
+ return params;
+ }
+
+ public void setParams(Map<String, String> params) {
+ this.params = params;
+ }
+
+ @CheckForNull
+ public String getTemplateRuleKey() {
+ return templateRuleKey;
+ }
+
+ public void setTemplateRuleKey(@Nullable String templateRuleKey) {
+ this.templateRuleKey = templateRuleKey;
+ }
+
+ public String getInternalKey() {
+ return internalKey;
+ }
+
+ public void setInternalKey(String internalKey) {
+ this.internalKey = internalKey;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java
new file mode 100644
index 00000000000..5a671de840f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.sonar.api.utils.DateUtils;
+
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+import com.google.common.collect.ImmutableMap;
+import org.sonar.api.batch.BatchSide;
+
+import javax.annotation.CheckForNull;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Lists the Quality profiles enabled on the current module.
+ */
+@BatchSide
+public class ModuleQProfiles {
+
+ public static final String SONAR_PROFILE_PROP = "sonar.profile";
+ private final Map<String, QProfile> byLanguage;
+
+ public ModuleQProfiles(Collection<QualityProfile> profiles) {
+ ImmutableMap.Builder<String, QProfile> builder = ImmutableMap.builder();
+
+ for (QualityProfile qProfile : profiles) {
+ builder.put(qProfile.getLanguage(),
+ new QProfile()
+ .setKey(qProfile.getKey())
+ .setName(qProfile.getName())
+ .setLanguage(qProfile.getLanguage())
+ .setRulesUpdatedAt(DateUtils.parseDateTime(qProfile.getRulesUpdatedAt())));
+ }
+ byLanguage = builder.build();
+ }
+
+ public Collection<QProfile> findAll() {
+ return byLanguage.values();
+ }
+
+ @CheckForNull
+ public QProfile findByLanguage(String language) {
+ return byLanguage.get(language);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfile.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfile.java
new file mode 100644
index 00000000000..5abc9955c7d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfile.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import com.google.common.base.Objects;
+
+import java.util.Date;
+
+public class QProfile {
+
+ private String key;
+ private String name;
+ private String language;
+ private Date rulesUpdatedAt;
+
+ public String getKey() {
+ return key;
+ }
+
+ public QProfile setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public QProfile setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public QProfile setLanguage(String language) {
+ this.language = language;
+ return this;
+ }
+
+ public Date getRulesUpdatedAt() {
+ return rulesUpdatedAt;
+ }
+
+ public QProfile setRulesUpdatedAt(Date d) {
+ this.rulesUpdatedAt = d;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ QProfile qProfile = (QProfile) o;
+ return key.equals(qProfile.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("key", key)
+ .add("name", name)
+ .add("language", language)
+ .add("rulesUpdatedAt", rulesUpdatedAt)
+ .toString();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileSensor.java
new file mode 100644
index 00000000000..bdffd6f1f5d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileSensor.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Project;
+
+/**
+ * Stores which Quality profiles have been used on the current module.
+ *
+ * TODO This information should not be stored as a measure but should be send as metadata in the {@link org.sonar.scanner.protocol.output.ScannerReport}
+ */
+public class QProfileSensor implements Sensor {
+
+ private final ModuleQProfiles moduleQProfiles;
+ private final FileSystem fs;
+ private final AnalysisMode analysisMode;
+
+ public QProfileSensor(ModuleQProfiles moduleQProfiles, FileSystem fs, AnalysisMode analysisMode) {
+ this.moduleQProfiles = moduleQProfiles;
+ this.fs = fs;
+ this.analysisMode = analysisMode;
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ // Should be only executed on leaf modules
+ return project.getModules().isEmpty()
+ // Useless in issues mode
+ && !analysisMode.isIssues();
+ }
+
+ @Override
+ public void analyse(Project project, SensorContext context) {
+ UsedQProfiles used = new UsedQProfiles();
+ for (String language : fs.languages()) {
+ QProfile profile = moduleQProfiles.findByLanguage(language);
+ if (profile != null) {
+ used.add(profile);
+ }
+ }
+ Measure<?> detailsMeasure = new Measure<>(CoreMetrics.QUALITY_PROFILES, used.toJson());
+ context.saveMeasure(detailsMeasure);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileVerifier.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileVerifier.java
new file mode 100644
index 00000000000..6206a289f17
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/QProfileVerifier.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+
+@BatchSide
+public class QProfileVerifier {
+
+ private static final Logger LOG = LoggerFactory.getLogger(QProfileVerifier.class);
+
+ private final Settings settings;
+ private final FileSystem fs;
+ private final ModuleQProfiles profiles;
+
+ public QProfileVerifier(Settings settings, FileSystem fs, ModuleQProfiles profiles) {
+ this.settings = settings;
+ this.fs = fs;
+ this.profiles = profiles;
+ }
+
+ public void execute() {
+ execute(LOG);
+ }
+
+ @VisibleForTesting
+ void execute(Logger logger) {
+ String defaultName = settings.getString(ModuleQProfiles.SONAR_PROFILE_PROP);
+ boolean defaultNameUsed = StringUtils.isBlank(defaultName);
+ for (String lang : fs.languages()) {
+ QProfile profile = profiles.findByLanguage(lang);
+ if (profile == null) {
+ logger.warn("No Quality profile found for language " + lang);
+ } else {
+ logger.info("Quality profile for {}: {}", lang, profile.getName());
+ if (isNotEmpty(defaultName) && defaultName.equals(profile.getName())) {
+ defaultNameUsed = true;
+ }
+ }
+ }
+ if (!defaultNameUsed && !fs.languages().isEmpty()) {
+ throw MessageException.of("sonar.profile was set to '" + defaultName + "' but didn't match any profile for any language. Please check your configuration.");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RuleFinderCompatibility.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RuleFinderCompatibility.java
new file mode 100644
index 00000000000..fe763f873bd
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RuleFinderCompatibility.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.sonar.api.batch.rule.Rules;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+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 javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * FIXME Waiting for the list of all server rules on batch side this is implemented by redirecting on ActiveRules. This is not correct
+ * since there is a difference between a rule that doesn't exists and a rule that is not activated in project quality profile.
+ *
+ */
+public class RuleFinderCompatibility implements RuleFinder {
+
+ private final Rules rules;
+ private static Function<org.sonar.api.batch.rule.Rule, Rule> ruleTransformer = new Function<org.sonar.api.batch.rule.Rule, Rule>() {
+ @Override
+ public Rule apply(@Nonnull org.sonar.api.batch.rule.Rule input) {
+ return toRule(input);
+ }
+ };
+
+ public RuleFinderCompatibility(Rules rules) {
+ this.rules = rules;
+ }
+
+ @Override
+ public Rule findById(int ruleId) {
+ throw new UnsupportedOperationException("Unable to find rule by id");
+ }
+
+ @Override
+ public Rule findByKey(String repositoryKey, String key) {
+ return findByKey(RuleKey.of(repositoryKey, key));
+ }
+
+ @Override
+ public Rule findByKey(RuleKey key) {
+ return toRule(rules.find(key));
+ }
+
+ @Override
+ public Rule find(RuleQuery query) {
+ Collection<Rule> all = findAll(query);
+ if (all.size() > 1) {
+ throw new IllegalArgumentException("Non unique result for rule query: " + ReflectionToStringBuilder.toString(query, ToStringStyle.SHORT_PREFIX_STYLE));
+ } else if (all.isEmpty()) {
+ return null;
+ } else {
+ return all.iterator().next();
+ }
+ }
+
+ @Override
+ public Collection<Rule> findAll(RuleQuery query) {
+ if (query.getConfigKey() != null) {
+ if (query.getRepositoryKey() != null && query.getKey() == null) {
+ return byInternalKey(query);
+ }
+ } else if (query.getRepositoryKey() != null) {
+ if (query.getKey() != null) {
+ return byKey(query);
+ } else {
+ return byRepository(query);
+ }
+ }
+ throw new UnsupportedOperationException("Unable to find rule by query");
+ }
+
+ private Collection<Rule> byRepository(RuleQuery query) {
+ return Collections2.transform(rules.findByRepository(query.getRepositoryKey()), ruleTransformer);
+ }
+
+
+
+ private Collection<Rule> byKey(RuleQuery query) {
+ Rule rule = toRule(rules.find(RuleKey.of(query.getRepositoryKey(), query.getKey())));
+ return rule != null ? Arrays.asList(rule) : Collections.<Rule>emptyList();
+ }
+
+ private Collection<Rule> byInternalKey(RuleQuery query) {
+ return Collections2.transform(rules.findByInternalKey(query.getRepositoryKey(), query.getConfigKey()), ruleTransformer);
+ }
+
+ @CheckForNull
+ private static Rule toRule(@Nullable org.sonar.api.batch.rule.Rule ar) {
+ return ar == null ? null : Rule.create(ar.key().repository(), ar.key().rule()).setName(ar.name()).setConfigKey(ar.internalKey());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesLoader.java
new file mode 100644
index 00000000000..9572f628f2d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesLoader.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonarqube.ws.Rules.ListResponse.Rule;
+
+public interface RulesLoader {
+ List<Rule> load(@Nullable MutableBoolean fromCache);
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileProvider.java
new file mode 100644
index 00000000000..de29cc352fc
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileProvider.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import java.util.Map;
+import org.apache.commons.lang.StringUtils;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.config.Settings;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.rules.ActiveRule;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RulePriority;
+
+/**
+ * Ensures backward-compatibility with extensions that use {@link org.sonar.api.profiles.RulesProfile}.
+ */
+public class RulesProfileProvider extends ProviderAdapter {
+
+ private RulesProfile singleton = null;
+
+ public RulesProfile provide(ModuleQProfiles qProfiles, ActiveRules activeRules, Settings settings) {
+ if (singleton == null) {
+ String lang = settings.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY);
+ if (StringUtils.isNotBlank(lang)) {
+ // Backward-compatibility with single-language modules
+ singleton = loadSingleLanguageProfile(qProfiles, activeRules, lang);
+ } else {
+ singleton = loadProfiles(qProfiles, activeRules);
+ }
+ }
+ return singleton;
+ }
+
+ private static RulesProfile loadSingleLanguageProfile(ModuleQProfiles qProfiles, ActiveRules activeRules, String language) {
+ QProfile qProfile = qProfiles.findByLanguage(language);
+ if (qProfile != null) {
+ return new RulesProfileWrapper(select(qProfile, activeRules));
+ }
+ return new RulesProfileWrapper(Lists.<RulesProfile>newArrayList());
+ }
+
+ private static RulesProfile loadProfiles(ModuleQProfiles qProfiles, ActiveRules activeRules) {
+ Collection<RulesProfile> dtos = Lists.newArrayList();
+ for (QProfile qProfile : qProfiles.findAll()) {
+ dtos.add(select(qProfile, activeRules));
+ }
+ return new RulesProfileWrapper(dtos);
+ }
+
+ private static RulesProfile select(QProfile qProfile, ActiveRules activeRules) {
+ RulesProfile deprecatedProfile = new RulesProfile();
+ // TODO deprecatedProfile.setVersion(qProfile.version());
+ deprecatedProfile.setName(qProfile.getName());
+ deprecatedProfile.setLanguage(qProfile.getLanguage());
+ for (org.sonar.api.batch.rule.ActiveRule activeRule : activeRules.findByLanguage(qProfile.getLanguage())) {
+ Rule rule = Rule.create(activeRule.ruleKey().repository(), activeRule.ruleKey().rule());
+ rule.setConfigKey(activeRule.internalKey());
+
+ // SONAR-6706
+ if (activeRule.templateRuleKey() != null) {
+ rule.setTemplate(Rule.create(activeRule.ruleKey().repository(), activeRule.templateRuleKey()));
+ }
+
+ ActiveRule deprecatedActiveRule = deprecatedProfile.activateRule(rule,
+ RulePriority.valueOf(activeRule.severity()));
+ for (Map.Entry<String, String> param : activeRule.params().entrySet()) {
+ rule.createParameter(param.getKey());
+ deprecatedActiveRule.setParameter(param.getKey(), param.getValue());
+ }
+ }
+ return deprecatedProfile;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileWrapper.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileWrapper.java
new file mode 100644
index 00000000000..327c141ea34
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProfileWrapper.java
@@ -0,0 +1,140 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.rules.ActiveRule;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.utils.SonarException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * This wrapper is used to try to preserve backward compatibility for plugins that used to
+ * depends on {@link org.sonar.api.profiles.RulesProfile}
+ *
+ * @since 4.2
+ */
+public class RulesProfileWrapper extends RulesProfile {
+
+ private static final Logger LOG = LoggerFactory.getLogger(RulesProfileWrapper.class);
+ private static final String DEPRECATED_USAGE_MESSAGE = "Please update your plugin to support multi-language analysis";
+
+ private final Collection<RulesProfile> profiles;
+ private final RulesProfile singleLanguageProfile;
+
+ public RulesProfileWrapper(Collection<RulesProfile> profiles) {
+ this.profiles = profiles;
+ this.singleLanguageProfile = null;
+ }
+
+ public RulesProfileWrapper(RulesProfile profile) {
+ this.profiles = Lists.newArrayList(profile);
+ this.singleLanguageProfile = profile;
+ }
+
+ @Override
+ public Integer getId() {
+ return getSingleProfileOrFail().getId();
+ }
+
+ private RulesProfile getSingleProfileOrFail() {
+ if (singleLanguageProfile == null) {
+ throw new IllegalStateException(DEPRECATED_USAGE_MESSAGE);
+ }
+ return singleLanguageProfile;
+ }
+
+ @Override
+ public String getName() {
+ return singleLanguageProfile != null ? singleLanguageProfile.getName() : "SonarQube";
+ }
+
+ @Override
+ public String getLanguage() {
+ if (singleLanguageProfile == null) {
+ // Multi-languages module
+ // This is a hack for CommonChecksDecorator that call this method in its constructor
+ LOG.debug(DEPRECATED_USAGE_MESSAGE, new SonarException(DEPRECATED_USAGE_MESSAGE));
+ return "";
+ }
+ return singleLanguageProfile.getLanguage();
+ }
+
+ @Override
+ public List<ActiveRule> getActiveRules() {
+ List<ActiveRule> activeRules = new ArrayList<>();
+ for (RulesProfile profile : profiles) {
+ activeRules.addAll(profile.getActiveRules());
+ }
+ return activeRules;
+ }
+
+ @Override
+ public ActiveRule getActiveRule(String repositoryKey, String ruleKey) {
+ for (RulesProfile profile : profiles) {
+ ActiveRule activeRule = profile.getActiveRule(repositoryKey, ruleKey);
+ if (activeRule != null) {
+ return activeRule;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<ActiveRule> getActiveRulesByRepository(String repositoryKey) {
+ List<ActiveRule> activeRules = new ArrayList<>();
+ for (RulesProfile profile : profiles) {
+ activeRules.addAll(profile.getActiveRulesByRepository(repositoryKey));
+ }
+ return activeRules;
+ }
+
+ @Override
+ public List<ActiveRule> getActiveRules(boolean acceptDisabledRules) {
+ List<ActiveRule> activeRules = new ArrayList<>();
+ for (RulesProfile profile : profiles) {
+ activeRules.addAll(profile.getActiveRules(acceptDisabledRules));
+ }
+ return activeRules;
+ }
+
+ @Override
+ public ActiveRule getActiveRule(Rule rule) {
+ for (RulesProfile profile : profiles) {
+ ActiveRule activeRule = profile.getActiveRule(rule);
+ if (activeRule != null) {
+ return activeRule;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Boolean getDefaultProfile() {
+ return getSingleProfileOrFail().getDefaultProfile();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProvider.java
new file mode 100644
index 00000000000..7a3e0ea34f0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/RulesProvider.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+
+import java.util.List;
+
+import org.sonarqube.ws.Rules.ListResponse.Rule;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.batch.rule.internal.RulesBuilder;
+import org.sonar.api.batch.rule.internal.NewRule;
+import org.sonar.api.batch.rule.Rules;
+
+public class RulesProvider extends ProviderAdapter {
+ private static final Logger LOG = Loggers.get(RulesProvider.class);
+ private static final String LOG_MSG = "Load server rules";
+ private Rules singleton = null;
+
+ public Rules provide(RulesLoader ref) {
+ if (singleton == null) {
+ singleton = load(ref);
+ }
+ return singleton;
+ }
+
+ private static Rules load(RulesLoader ref) {
+ Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+ MutableBoolean fromCache = new MutableBoolean();
+ List<Rule> loadedRules = ref.load(fromCache);
+ RulesBuilder builder = new RulesBuilder();
+
+ for (Rule r : loadedRules) {
+ NewRule newRule = builder.add(RuleKey.of(r.getRepository(), r.getKey()));
+ newRule.setName(r.getName());
+ newRule.setInternalKey(r.getInternalKey());
+ }
+
+ profiler.stopInfo(fromCache.booleanValue());
+
+ return builder.build();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/UsedQProfiles.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/UsedQProfiles.java
new file mode 100644
index 00000000000..72186a5e959
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/UsedQProfiles.java
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import com.google.common.collect.Sets;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.core.util.UtcDateUtils;
+
+import javax.annotation.concurrent.Immutable;
+
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+
+@Immutable
+public class UsedQProfiles {
+
+ private final SortedSet<QProfile> profiles = Sets.newTreeSet(new Comparator<QProfile>() {
+ @Override
+ public int compare(QProfile o1, QProfile o2) {
+ int c = o1.getLanguage().compareTo(o2.getLanguage());
+ if (c == 0) {
+ c = o1.getName().compareTo(o2.getName());
+ }
+ return c;
+ }
+ });
+
+ public static UsedQProfiles fromJson(String json) {
+ UsedQProfiles result = new UsedQProfiles();
+ JsonArray jsonRoot = new JsonParser().parse(json).getAsJsonArray();
+ for (JsonElement jsonElt : jsonRoot) {
+ JsonObject jsonProfile = jsonElt.getAsJsonObject();
+ QProfile profile = new QProfile();
+ profile.setKey(jsonProfile.get("key").getAsString());
+ profile.setName(jsonProfile.get("name").getAsString());
+ profile.setLanguage(jsonProfile.get("language").getAsString());
+ profile.setRulesUpdatedAt(UtcDateUtils.parseDateTime(jsonProfile.get("rulesUpdatedAt").getAsString()));
+ result.add(profile);
+ }
+ return result;
+ }
+
+ public String toJson() {
+ StringWriter json = new StringWriter();
+ JsonWriter writer = JsonWriter.of(json);
+ writer.beginArray();
+ for (QProfile profile : profiles) {
+ writer
+ .beginObject()
+ .prop("key", profile.getKey())
+ .prop("language", profile.getLanguage())
+ .prop("name", profile.getName())
+ .prop("rulesUpdatedAt", UtcDateUtils.formatDateTime(profile.getRulesUpdatedAt()))
+ .endObject();
+ }
+ writer.endArray();
+ writer.close();
+ return json.toString();
+ }
+
+ public UsedQProfiles add(UsedQProfiles other) {
+ addAll(other.profiles);
+ return this;
+ }
+
+ public UsedQProfiles add(QProfile profile) {
+ profiles.add(profile);
+ return this;
+ }
+
+ public UsedQProfiles addAll(Collection<QProfile> profiles) {
+ this.profiles.addAll(profiles);
+ return this;
+ }
+
+ public SortedSet<QProfile> profiles() {
+ return profiles;
+ }
+
+ public Map<String, QProfile> profilesByKey() {
+ Map<String, QProfile> map = new HashMap<>();
+ for (QProfile profile : profiles) {
+ map.put(profile.getKey(), profile);
+ }
+ return map;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/package-info.java
new file mode 100644
index 00000000000..1bdc402f399
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/rule/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.rule;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactor.java
new file mode 100644
index 00000000000..4bfa99c6da2
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactor.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+
+/**
+ * Immutable copy of project reactor after all modifications have been applied (see {@link ImmutableProjectReactorProvider}).
+ */
+@BatchSide
+public class ImmutableProjectReactor {
+
+ private ProjectDefinition root;
+ private Map<String, ProjectDefinition> byKey = new LinkedHashMap<>();
+
+ public ImmutableProjectReactor(ProjectDefinition root) {
+ if (root.getParent() != null) {
+ throw new IllegalArgumentException("Not a root project: " + root);
+ }
+ this.root = root;
+ collectProjects(root);
+ }
+
+ public Collection<ProjectDefinition> getProjects() {
+ return byKey.values();
+ }
+
+ /**
+ * Populates list of projects from hierarchy.
+ */
+ private void collectProjects(ProjectDefinition def) {
+ if (byKey.containsKey(def.getKeyWithBranch())) {
+ throw new IllegalStateException("Duplicate module key in reactor: " + def.getKeyWithBranch());
+ }
+ byKey.put(def.getKeyWithBranch(), def);
+ for (ProjectDefinition child : def.getSubProjects()) {
+ collectProjects(child);
+ }
+ }
+
+ public ProjectDefinition getRoot() {
+ return root;
+ }
+
+ @CheckForNull
+ public ProjectDefinition getProjectDefinition(String keyWithBranch) {
+ return byKey.get(keyWithBranch);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactorProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactorProvider.java
new file mode 100644
index 00000000000..ecf58a88482
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ImmutableProjectReactorProvider.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+
+public class ImmutableProjectReactorProvider extends ProviderAdapter {
+
+ private ImmutableProjectReactor singleton;
+
+ public ImmutableProjectReactor provide(ProjectReactor reactor, ProjectBuildersExecutor projectBuildersExecutor, ProjectExclusions exclusions, ProjectReactorValidator validator) {
+ if (singleton == null) {
+ // 1 Apply project builders
+ projectBuildersExecutor.execute(reactor);
+
+ // 2 Apply project exclusions
+ exclusions.apply(reactor);
+
+ // 3 Validate final reactor
+ validator.validate(reactor);
+
+ singleton = new ImmutableProjectReactor(reactor.getRoot());
+ }
+ return singleton;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/LanguageVerifier.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/LanguageVerifier.java
new file mode 100644
index 00000000000..83e27324e64
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/LanguageVerifier.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.sonar.batch.repository.language.Language;
+import org.sonar.batch.repository.language.LanguagesRepository;
+import org.apache.commons.lang.StringUtils;
+import org.picocontainer.Startable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+
+/**
+ * Verifies that the property sonar.language is valid
+ */
+public class LanguageVerifier implements Startable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LanguageVerifier.class);
+
+ private final Settings settings;
+ private final LanguagesRepository languages;
+ private final DefaultFileSystem fs;
+
+ public LanguageVerifier(Settings settings, LanguagesRepository languages, DefaultFileSystem fs) {
+ this.settings = settings;
+ this.languages = languages;
+ this.fs = fs;
+ }
+
+ @Override
+ public void start() {
+ String languageKey = settings.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY);
+ if (StringUtils.isNotBlank(languageKey)) {
+ LOG.info("Language is forced to {}", languageKey);
+ Language language = languages.get(languageKey);
+ if (language == null) {
+ throw MessageException.of("You must install a plugin that supports the language '" + languageKey + "'");
+ }
+
+ // force the registration of the language, even if there are no related source files
+ fs.addLanguages(languageKey);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
new file mode 100644
index 00000000000..56da7fd624d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.batch.rule.CheckFactory;
+import org.sonar.api.resources.Project;
+import org.sonar.api.scan.filesystem.FileExclusions;
+import org.sonar.batch.DefaultProjectTree;
+import org.sonar.batch.bootstrap.BatchExtensionDictionnary;
+import org.sonar.batch.bootstrap.ExtensionInstaller;
+import org.sonar.batch.bootstrap.ExtensionMatcher;
+import org.sonar.batch.bootstrap.ExtensionUtils;
+import org.sonar.batch.deprecated.DeprecatedSensorContext;
+import org.sonar.batch.deprecated.perspectives.BatchPerspectives;
+import org.sonar.batch.events.EventBus;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.index.DefaultIndex;
+import org.sonar.batch.issue.IssuableFactory;
+import org.sonar.batch.issue.IssueFilters;
+import org.sonar.batch.issue.ModuleIssues;
+import org.sonar.batch.issue.ignore.EnforceIssuesFilter;
+import org.sonar.batch.issue.ignore.IgnoreIssuesFilter;
+import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssueInclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.scanner.IssueExclusionsLoader;
+import org.sonar.batch.issue.ignore.scanner.IssueExclusionsRegexpScanner;
+import org.sonar.batch.phases.AbstractPhaseExecutor;
+import org.sonar.batch.phases.InitializersExecutor;
+import org.sonar.batch.phases.IssuesPhaseExecutor;
+import org.sonar.batch.phases.PostJobsExecutor;
+import org.sonar.batch.phases.ProjectInitializer;
+import org.sonar.batch.phases.PublishPhaseExecutor;
+import org.sonar.batch.phases.SensorsExecutor;
+import org.sonar.batch.postjob.DefaultPostJobContext;
+import org.sonar.batch.postjob.PostJobOptimizer;
+import org.sonar.batch.rule.QProfileSensor;
+import org.sonar.batch.rule.QProfileVerifier;
+import org.sonar.batch.rule.RuleFinderCompatibility;
+import org.sonar.batch.rule.RulesProfileProvider;
+import org.sonar.batch.scan.filesystem.ComponentIndexer;
+import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem;
+import org.sonar.batch.scan.filesystem.DeprecatedFileFilters;
+import org.sonar.batch.scan.filesystem.ExclusionFilters;
+import org.sonar.batch.scan.filesystem.FileIndexer;
+import org.sonar.batch.scan.filesystem.FileSystemLogger;
+import org.sonar.batch.scan.filesystem.InputFileBuilderFactory;
+import org.sonar.batch.scan.filesystem.LanguageDetectionFactory;
+import org.sonar.batch.scan.filesystem.ModuleFileSystemInitializer;
+import org.sonar.batch.scan.filesystem.ModuleInputFileCache;
+import org.sonar.batch.scan.filesystem.StatusDetectionFactory;
+import org.sonar.batch.scan.report.IssuesReports;
+import org.sonar.batch.sensor.DefaultSensorStorage;
+import org.sonar.batch.sensor.SensorOptimizer;
+import org.sonar.batch.sensor.coverage.CoverageExclusions;
+import org.sonar.batch.source.HighlightableBuilder;
+import org.sonar.batch.source.SymbolizableBuilder;
+import org.sonar.core.platform.ComponentContainer;
+
+public class ModuleScanContainer extends ComponentContainer {
+ private static final Logger LOG = LoggerFactory.getLogger(ModuleScanContainer.class);
+ private final Project module;
+
+ public ModuleScanContainer(ProjectScanContainer parent, Project module) {
+ super(parent);
+ this.module = module;
+ }
+
+ @Override
+ protected void doBeforeStart() {
+ LOG.info("------------- Scan {}", module.getName());
+ addCoreComponents();
+ addExtensions();
+ }
+
+ private void addCoreComponents() {
+ ProjectDefinition moduleDefinition = getComponentByType(DefaultProjectTree.class).getProjectDefinition(module);
+ add(
+ moduleDefinition,
+ module,
+ getComponentByType(BatchComponentCache.class).get(module).inputComponent(),
+ ModuleSettings.class);
+
+ // hack to initialize settings before ExtensionProviders
+ ModuleSettings moduleSettings = getComponentByType(ModuleSettings.class);
+ module.setSettings(moduleSettings);
+
+ if (getComponentByType(AnalysisMode.class).isIssues()) {
+ add(IssuesPhaseExecutor.class,
+ IssuesReports.class);
+ } else {
+ add(PublishPhaseExecutor.class);
+ }
+
+ add(
+ EventBus.class,
+ RuleFinderCompatibility.class,
+ PostJobsExecutor.class,
+ SensorsExecutor.class,
+ InitializersExecutor.class,
+ ProjectInitializer.class,
+
+ // file system
+ ModuleInputFileCache.class,
+ FileExclusions.class,
+ ExclusionFilters.class,
+ DeprecatedFileFilters.class,
+ InputFileBuilderFactory.class,
+ FileMetadata.class,
+ StatusDetectionFactory.class,
+ LanguageDetectionFactory.class,
+ FileIndexer.class,
+ ComponentIndexer.class,
+ LanguageVerifier.class,
+ FileSystemLogger.class,
+ DefaultModuleFileSystem.class,
+ ModuleFileSystemInitializer.class,
+ QProfileVerifier.class,
+
+ SensorOptimizer.class,
+ PostJobOptimizer.class,
+
+ DefaultPostJobContext.class,
+ DefaultSensorStorage.class,
+ DeprecatedSensorContext.class,
+ BatchExtensionDictionnary.class,
+ IssueFilters.class,
+ CoverageExclusions.class,
+
+ // rules
+ new RulesProfileProvider(),
+ QProfileSensor.class,
+ CheckFactory.class,
+
+ // issues
+ IssuableFactory.class,
+ ModuleIssues.class,
+ org.sonar.api.issue.NoSonarFilter.class,
+
+ // issue exclusions
+ IssueInclusionPatternInitializer.class,
+ IssueExclusionPatternInitializer.class,
+ IssueExclusionsRegexpScanner.class,
+ IssueExclusionsLoader.class,
+ EnforceIssuesFilter.class,
+ IgnoreIssuesFilter.class,
+
+ // Perspectives
+ BatchPerspectives.class,
+ HighlightableBuilder.class,
+ SymbolizableBuilder.class);
+ }
+
+ private void addExtensions() {
+ ExtensionInstaller installer = getComponentByType(ExtensionInstaller.class);
+ installer.install(this, new ExtensionMatcher() {
+ @Override
+ public boolean accept(Object extension) {
+ return ExtensionUtils.isBatchSide(extension) && ExtensionUtils.isInstantiationStrategy(extension, InstantiationStrategy.PER_PROJECT);
+ }
+ });
+ }
+
+ @Override
+ protected void doAfterStart() {
+ DefaultIndex index = getComponentByType(DefaultIndex.class);
+ index.setCurrentProject(module, getComponentByType(DefaultSensorStorage.class));
+
+ getComponentByType(AbstractPhaseExecutor.class).execute(module);
+
+ // Free memory since module settings are no more used
+ module.setSettings(null);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleSettings.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleSettings.java
new file mode 100644
index 00000000000..2176ad347fe
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ModuleSettings.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.GlobalSettings;
+import org.sonar.batch.report.AnalysisContextReportPublisher;
+import org.sonar.batch.repository.ProjectRepositories;
+
+/**
+ * @since 2.12
+ */
+public class ModuleSettings extends Settings {
+
+ private final ProjectRepositories projectRepos;
+ private final DefaultAnalysisMode analysisMode;
+
+ public ModuleSettings(GlobalSettings batchSettings, ProjectDefinition moduleDefinition, ProjectRepositories projectSettingsRepo,
+ DefaultAnalysisMode analysisMode, AnalysisContextReportPublisher contextReportPublisher) {
+ super(batchSettings.getDefinitions());
+ this.projectRepos = projectSettingsRepo;
+ this.analysisMode = analysisMode;
+ getEncryption().setPathToSecretKey(batchSettings.getString(CoreProperties.ENCRYPTION_SECRET_KEY_PATH));
+
+ init(moduleDefinition, batchSettings);
+ contextReportPublisher.dumpSettings(moduleDefinition);
+ }
+
+ private ModuleSettings init(ProjectDefinition moduleDefinition, GlobalSettings batchSettings) {
+ addProjectProperties(moduleDefinition, batchSettings);
+ addBuildProperties(moduleDefinition);
+ return this;
+ }
+
+ private void addProjectProperties(ProjectDefinition def, GlobalSettings batchSettings) {
+ addProperties(batchSettings.getProperties());
+ do {
+ if (projectRepos.moduleExists(def.getKeyWithBranch())) {
+ addProperties(projectRepos.settings(def.getKeyWithBranch()));
+ break;
+ }
+ def = def.getParent();
+ } while (def != null);
+ }
+
+ private void addBuildProperties(ProjectDefinition project) {
+ List<ProjectDefinition> orderedProjects = getTopDownParentProjects(project);
+ for (ProjectDefinition p : orderedProjects) {
+ addProperties(p.properties());
+ }
+ }
+
+ /**
+ * From root to given project
+ */
+ static List<ProjectDefinition> getTopDownParentProjects(ProjectDefinition project) {
+ List<ProjectDefinition> result = Lists.newArrayList();
+ ProjectDefinition p = project;
+ while (p != null) {
+ result.add(0, p);
+ p = p.getParent();
+ }
+ return result;
+ }
+
+ @Override
+ protected void doOnGetProperties(String key) {
+ if (analysisMode.isIssues() && key.endsWith(".secured") && !key.contains(".license")) {
+ throw MessageException.of("Access to the secured property '" + key
+ + "' is not possible in issues mode. The SonarQube plugin which requires this property must be deactivated in issues mode.");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/MutableProjectReactorProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/MutableProjectReactorProvider.java
new file mode 100644
index 00000000000..afde1e18c4d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/MutableProjectReactorProvider.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+
+public class MutableProjectReactorProvider extends ProviderAdapter {
+ private ProjectReactor reactor = null;
+
+ public ProjectReactor provide(ProjectReactorBuilder builder) {
+ if (reactor == null) {
+ reactor = builder.execute();
+ }
+ return reactor;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectBuildersExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectBuildersExecutor.java
new file mode 100644
index 00000000000..505a80bc117
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectBuildersExecutor.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.sonar.api.batch.bootstrap.ProjectBuilder;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.batch.bootstrap.internal.ProjectBuilderContext;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+
+public class ProjectBuildersExecutor {
+
+ private static final Logger LOG = Loggers.get(ProjectBuildersExecutor.class);
+
+ private final ProjectBuilder[] projectBuilders;
+
+ public ProjectBuildersExecutor(ProjectBuilder... projectBuilders) {
+ this.projectBuilders = projectBuilders;
+ }
+
+ public ProjectBuildersExecutor() {
+ this(new ProjectBuilder[0]);
+ }
+
+ public void execute(ProjectReactor reactor) {
+ if (projectBuilders.length > 0) {
+ Profiler profiler = Profiler.create(LOG).startInfo("Execute project builders");
+ ProjectBuilderContext context = new ProjectBuilderContext(reactor);
+
+ for (ProjectBuilder projectBuilder : projectBuilders) {
+ projectBuilder.build(context);
+ }
+ profiler.stopInfo();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectExclusions.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectExclusions.java
new file mode 100644
index 00000000000..0bea9a81309
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectExclusions.java
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.config.Settings;
+
+/**
+ * Exclude the sub-projects as defined by the properties sonar.skippedModules and sonar.includedModules
+ *
+ * @since 2.12
+ */
+public class ProjectExclusions {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ProjectExclusions.class);
+
+ private Settings settings;
+
+ public ProjectExclusions(Settings settings) {
+ this.settings = settings;
+ }
+
+ public void apply(ProjectReactor reactor) {
+ if (!reactor.getProjects().isEmpty() && StringUtils.isNotBlank(reactor.getProjects().get(0).getKey())) {
+ LOG.info("Apply project exclusions");
+
+ if (settings.hasKey(CoreProperties.CORE_INCLUDED_MODULES_PROPERTY)) {
+ LOG.warn("'sonar.includedModules' property is deprecated since version 4.3 and should not be used anymore.");
+ }
+ if (settings.hasKey(CoreProperties.CORE_SKIPPED_MODULES_PROPERTY)) {
+ LOG.warn("'sonar.skippedModules' property is deprecated since version 4.3 and should not be used anymore.");
+ }
+
+ for (ProjectDefinition project : reactor.getProjects()) {
+ if (isExcluded(key(project), project == reactor.getRoot())) {
+ exclude(project);
+ }
+ }
+ }
+ }
+
+ private boolean isExcluded(String projectKey, boolean isRoot) {
+ String[] includedKeys = settings.getStringArray(CoreProperties.CORE_INCLUDED_MODULES_PROPERTY);
+ boolean excluded = false;
+ if (!isRoot && includedKeys.length > 0) {
+ excluded = !ArrayUtils.contains(includedKeys, projectKey);
+ }
+ String skippedModulesProperty = CoreProperties.CORE_SKIPPED_MODULES_PROPERTY;
+ if (!excluded) {
+ String[] excludedKeys = settings.getStringArray(skippedModulesProperty);
+ excluded = ArrayUtils.contains(excludedKeys, projectKey);
+ }
+ if (excluded && isRoot) {
+ throw new IllegalArgumentException("The root project can't be excluded. Please check the parameters " + skippedModulesProperty + " and sonar.includedModules.");
+ }
+ return excluded;
+ }
+
+ private void exclude(ProjectDefinition project) {
+ LOG.info(String.format("Exclude project: %s [%s]", project.getName(), project.getKey()));
+ project.remove();
+ }
+
+ static String key(ProjectDefinition project) {
+ String key = project.getKey();
+ if (key.contains(":")) {
+ return StringUtils.substringAfter(key, ":");
+ }
+ return key;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectLock.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectLock.java
new file mode 100644
index 00000000000..d52f49cfd88
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectLock.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.sonar.batch.bootstrap.Slf4jLogger;
+import org.sonar.home.cache.DirectoryLock;
+import org.picocontainer.Startable;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+
+import java.io.IOException;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class ProjectLock implements Startable {
+ private final DirectoryLock lock;
+
+ public ProjectLock(ProjectReactor projectReactor) {
+ Path directory = projectReactor.getRoot().getWorkDir().toPath();
+ try {
+ if (!Files.exists(directory)) {
+ Files.createDirectories(directory);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create work directory", e);
+ }
+ this.lock = new DirectoryLock(directory.toAbsolutePath(), new Slf4jLogger());
+ }
+
+ public void tryLock() {
+ try {
+ if (!lock.tryLock()) {
+ failAlreadyInProgress(null);
+ }
+ } catch (OverlappingFileLockException e) {
+ failAlreadyInProgress(e);
+ }
+ }
+
+ private static void failAlreadyInProgress(Exception e) {
+ throw new IllegalStateException("Another SonarQube analysis is already in progress for this project", e);
+ }
+
+ @Override
+ public void stop() {
+ lock.unlock();
+ }
+
+ @Override
+ public void start() {
+ // nothing to do
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorBuilder.java
new file mode 100644
index 00000000000..2d7e453cfaf
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorBuilder.java
@@ -0,0 +1,423 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.batch.analysis.AnalysisProperties;
+import org.sonar.batch.bootstrap.DroppedPropertyChecker;
+import org.sonar.batch.util.BatchUtils;
+
+/**
+ * Class that creates a project definition based on a set of properties.
+ */
+public class ProjectReactorBuilder {
+
+ private static final String INVALID_VALUE_OF_X_FOR_Y = "Invalid value of {0} for {1}";
+
+ /**
+ * A map of dropped properties as key and specific message to display for that property
+ * (what will happen, what should the user do, ...) as a value
+ */
+ private static final Map<String, String> DROPPED_PROPERTIES = ImmutableMap.of(
+ "sonar.qualitygate", "It will be ignored.");
+
+ private static final Logger LOG = Loggers.get(ProjectReactorBuilder.class);
+
+ /**
+ * @since 4.1 but not yet exposed in {@link CoreProperties}
+ */
+ private static final String MODULE_KEY_PROPERTY = "sonar.moduleKey";
+
+ protected static final String PROPERTY_PROJECT_BASEDIR = "sonar.projectBaseDir";
+ private static final String PROPERTY_PROJECT_BUILDDIR = "sonar.projectBuildDir";
+ private static final String PROPERTY_MODULES = "sonar.modules";
+
+ /**
+ * New properties, to be consistent with Sonar naming conventions
+ *
+ * @since 1.5
+ */
+ private static final String PROPERTY_SOURCES = "sonar.sources";
+ private static final String PROPERTY_TESTS = "sonar.tests";
+
+ /**
+ * Array of all mandatory properties required for a project without child.
+ */
+ private static final String[] MANDATORY_PROPERTIES_FOR_SIMPLE_PROJECT = {
+ PROPERTY_PROJECT_BASEDIR, CoreProperties.PROJECT_KEY_PROPERTY, CoreProperties.PROJECT_NAME_PROPERTY,
+ CoreProperties.PROJECT_VERSION_PROPERTY, PROPERTY_SOURCES
+ };
+
+ /**
+ * Array of all mandatory properties required for a project with children.
+ */
+ private static final String[] MANDATORY_PROPERTIES_FOR_MULTIMODULE_PROJECT = {PROPERTY_PROJECT_BASEDIR, CoreProperties.PROJECT_KEY_PROPERTY,
+ CoreProperties.PROJECT_NAME_PROPERTY, CoreProperties.PROJECT_VERSION_PROPERTY};
+
+ /**
+ * Array of all mandatory properties required for a child project before its properties get merged with its parent ones.
+ */
+ private static final String[] MANDATORY_PROPERTIES_FOR_CHILD = {MODULE_KEY_PROPERTY, CoreProperties.PROJECT_NAME_PROPERTY};
+
+ /**
+ * Properties that must not be passed from the parent project to its children.
+ */
+ private static final List<String> NON_HERITED_PROPERTIES_FOR_CHILD = Lists.newArrayList(PROPERTY_PROJECT_BASEDIR, CoreProperties.WORKING_DIRECTORY, PROPERTY_MODULES,
+ CoreProperties.PROJECT_DESCRIPTION_PROPERTY);
+
+ private static final String NON_ASSOCIATED_PROJECT_KEY = "project";
+
+ private final AnalysisProperties analysisProps;
+ private final AnalysisMode analysisMode;
+ private File rootProjectWorkDir;
+
+ public ProjectReactorBuilder(AnalysisProperties props, AnalysisMode analysisMode) {
+ this.analysisProps = props;
+ this.analysisMode = analysisMode;
+ }
+
+ public ProjectReactor execute() {
+ Profiler profiler = Profiler.create(LOG).startInfo("Process project properties");
+ new DroppedPropertyChecker(analysisProps.properties(), DROPPED_PROPERTIES).checkDroppedProperties();
+ Map<String, Map<String, String>> propertiesByModuleIdPath = new HashMap<>();
+ extractPropertiesByModule(propertiesByModuleIdPath, "", "", analysisProps.properties());
+ ProjectDefinition rootProject = defineRootProject(propertiesByModuleIdPath.get(""), null);
+ rootProjectWorkDir = rootProject.getWorkDir();
+ defineChildren(rootProject, propertiesByModuleIdPath, "");
+ cleanAndCheckProjectDefinitions(rootProject);
+ // Since task properties are now empty we should add root module properties
+ analysisProps.properties().putAll(propertiesByModuleIdPath.get(""));
+ profiler.stopDebug();
+ return new ProjectReactor(rootProject);
+ }
+
+ private static void extractPropertiesByModule(Map<String, Map<String, String>> propertiesByModuleIdPath, String currentModuleId, String currentModuleIdPath,
+ Map<String, String> parentProperties) {
+ if (propertiesByModuleIdPath.containsKey(currentModuleIdPath)) {
+ throw MessageException.of(String.format("Two modules have the same id: '%s'. Each module must have a unique id.", currentModuleId));
+ }
+
+ Map<String, String> currentModuleProperties = new HashMap<>();
+ String prefix = !currentModuleId.isEmpty() ? (currentModuleId + ".") : "";
+ int prefixLength = prefix.length();
+
+ // By default all properties starting with module prefix belong to current module
+ Iterator<Entry<String, String>> it = parentProperties.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String> e = it.next();
+ String key = e.getKey();
+ if (key.startsWith(prefix)) {
+ currentModuleProperties.put(key.substring(prefixLength), e.getValue());
+ it.remove();
+ }
+ }
+ String[] moduleIds = getListFromProperty(currentModuleProperties, PROPERTY_MODULES);
+ // Sort module by reverse lexicographic order to avoid issue when one module id is a prefix of another one
+ Arrays.sort(moduleIds);
+ ArrayUtils.reverse(moduleIds);
+
+ propertiesByModuleIdPath.put(currentModuleIdPath, currentModuleProperties);
+
+ for (String moduleId : moduleIds) {
+ String subModuleIdPath = currentModuleIdPath.isEmpty() ? moduleId : (currentModuleIdPath + "." + moduleId);
+ extractPropertiesByModule(propertiesByModuleIdPath, moduleId, subModuleIdPath, currentModuleProperties);
+ }
+ }
+
+ private static void prepareNonAssociatedProject(Map<String, String> props, AnalysisMode mode) {
+ if (mode.isIssues() && !props.containsKey(CoreProperties.PROJECT_KEY_PROPERTY)) {
+ props.put(CoreProperties.PROJECT_KEY_PROPERTY, NON_ASSOCIATED_PROJECT_KEY);
+ }
+ }
+
+ protected ProjectDefinition defineRootProject(Map<String, String> rootProperties, @Nullable ProjectDefinition parent) {
+ prepareNonAssociatedProject(rootProperties, analysisMode);
+
+ if (rootProperties.containsKey(PROPERTY_MODULES)) {
+ checkMandatoryProperties(rootProperties, MANDATORY_PROPERTIES_FOR_MULTIMODULE_PROJECT);
+ } else {
+ checkMandatoryProperties(rootProperties, MANDATORY_PROPERTIES_FOR_SIMPLE_PROJECT);
+ }
+ File baseDir = new File(rootProperties.get(PROPERTY_PROJECT_BASEDIR));
+ final String projectKey = rootProperties.get(CoreProperties.PROJECT_KEY_PROPERTY);
+ File workDir;
+ if (parent == null) {
+ validateDirectories(rootProperties, baseDir, projectKey);
+ workDir = initRootProjectWorkDir(baseDir, rootProperties);
+ } else {
+ workDir = initModuleWorkDir(baseDir, rootProperties);
+ }
+
+ return ProjectDefinition.create().setProperties(rootProperties)
+ .setBaseDir(baseDir)
+ .setWorkDir(workDir)
+ .setBuildDir(initModuleBuildDir(baseDir, rootProperties));
+ }
+
+ @VisibleForTesting
+ protected File initRootProjectWorkDir(File baseDir, Map<String, String> rootProperties) {
+ String workDir = rootProperties.get(CoreProperties.WORKING_DIRECTORY);
+ if (StringUtils.isBlank(workDir)) {
+ return new File(baseDir, CoreProperties.WORKING_DIRECTORY_DEFAULT_VALUE);
+ }
+
+ File customWorkDir = new File(workDir);
+ if (customWorkDir.isAbsolute()) {
+ return customWorkDir;
+ }
+ return new File(baseDir, customWorkDir.getPath());
+ }
+
+ @VisibleForTesting
+ protected File initModuleWorkDir(File moduleBaseDir, Map<String, String> moduleProperties) {
+ String workDir = moduleProperties.get(CoreProperties.WORKING_DIRECTORY);
+ if (StringUtils.isBlank(workDir)) {
+ return new File(rootProjectWorkDir, BatchUtils.cleanKeyForFilename(moduleProperties.get(CoreProperties.PROJECT_KEY_PROPERTY)));
+ }
+
+ File customWorkDir = new File(workDir);
+ if (customWorkDir.isAbsolute()) {
+ return customWorkDir;
+ }
+ return new File(moduleBaseDir, customWorkDir.getPath());
+ }
+
+ @CheckForNull
+ private static File initModuleBuildDir(File moduleBaseDir, Map<String, String> moduleProperties) {
+ String buildDir = moduleProperties.get(PROPERTY_PROJECT_BUILDDIR);
+ if (StringUtils.isBlank(buildDir)) {
+ return null;
+ }
+
+ File customBuildDir = new File(buildDir);
+ if (customBuildDir.isAbsolute()) {
+ return customBuildDir;
+ }
+ return new File(moduleBaseDir, customBuildDir.getPath());
+ }
+
+ private void defineChildren(ProjectDefinition parentProject, Map<String, Map<String, String>> propertiesByModuleIdPath, String parentModuleIdPath) {
+ Map<String, String> parentProps = parentProject.properties();
+ if (parentProps.containsKey(PROPERTY_MODULES)) {
+ for (String moduleId : getListFromProperty(parentProps, PROPERTY_MODULES)) {
+ String moduleIdPath = parentModuleIdPath.isEmpty() ? moduleId : (parentModuleIdPath + "." + moduleId);
+ Map<String, String> moduleProps = propertiesByModuleIdPath.get(moduleIdPath);
+ ProjectDefinition childProject = loadChildProject(parentProject, moduleProps, moduleId);
+ // check the uniqueness of the child key
+ checkUniquenessOfChildKey(childProject, parentProject);
+ // the child project may have children as well
+ defineChildren(childProject, propertiesByModuleIdPath, moduleIdPath);
+ // and finally add this child project to its parent
+ parentProject.addSubProject(childProject);
+ }
+ }
+ }
+
+ protected ProjectDefinition loadChildProject(ProjectDefinition parentProject, Map<String, String> moduleProps, String moduleId) {
+ final File baseDir;
+ if (moduleProps.containsKey(PROPERTY_PROJECT_BASEDIR)) {
+ baseDir = resolvePath(parentProject.getBaseDir(), moduleProps.get(PROPERTY_PROJECT_BASEDIR));
+ setProjectBaseDir(baseDir, moduleProps, moduleId);
+ } else {
+ baseDir = new File(parentProject.getBaseDir(), moduleId);
+ setProjectBaseDir(baseDir, moduleProps, moduleId);
+ }
+
+ setModuleKeyAndNameIfNotDefined(moduleProps, moduleId, parentProject.getKey());
+
+ // and finish
+ checkMandatoryProperties(moduleProps, MANDATORY_PROPERTIES_FOR_CHILD);
+ validateDirectories(moduleProps, baseDir, moduleId);
+
+ mergeParentProperties(moduleProps, parentProject.properties());
+
+ return defineRootProject(moduleProps, parentProject);
+ }
+
+ @VisibleForTesting
+ protected static void setModuleKeyAndNameIfNotDefined(Map<String, String> childProps, String moduleId, String parentKey) {
+ if (!childProps.containsKey(MODULE_KEY_PROPERTY)) {
+ if (!childProps.containsKey(CoreProperties.PROJECT_KEY_PROPERTY)) {
+ childProps.put(MODULE_KEY_PROPERTY, parentKey + ":" + moduleId);
+ } else {
+ String childKey = childProps.get(CoreProperties.PROJECT_KEY_PROPERTY);
+ childProps.put(MODULE_KEY_PROPERTY, parentKey + ":" + childKey);
+ }
+ }
+ if (!childProps.containsKey(CoreProperties.PROJECT_NAME_PROPERTY)) {
+ childProps.put(CoreProperties.PROJECT_NAME_PROPERTY, moduleId);
+ }
+ // For backward compatibility with ProjectDefinition
+ childProps.put(CoreProperties.PROJECT_KEY_PROPERTY, childProps.get(MODULE_KEY_PROPERTY));
+ }
+
+ @VisibleForTesting
+ protected static void checkUniquenessOfChildKey(ProjectDefinition childProject, ProjectDefinition parentProject) {
+ for (ProjectDefinition definition : parentProject.getSubProjects()) {
+ if (definition.getKey().equals(childProject.getKey())) {
+ throw MessageException.of("Project '" + parentProject.getKey() + "' can't have 2 modules with the following key: " + childProject.getKey());
+ }
+ }
+ }
+
+ protected static void setProjectBaseDir(File baseDir, Map<String, String> childProps, String moduleId) {
+ if (!baseDir.isDirectory()) {
+ throw MessageException.of("The base directory of the module '" + moduleId + "' does not exist: " + baseDir.getAbsolutePath());
+ }
+ childProps.put(PROPERTY_PROJECT_BASEDIR, baseDir.getAbsolutePath());
+ }
+
+ @VisibleForTesting
+ protected static void checkMandatoryProperties(Map<String, String> props, String[] mandatoryProps) {
+ StringBuilder missing = new StringBuilder();
+ for (String mandatoryProperty : mandatoryProps) {
+ if (!props.containsKey(mandatoryProperty)) {
+ if (missing.length() > 0) {
+ missing.append(", ");
+ }
+ missing.append(mandatoryProperty);
+ }
+ }
+ String moduleKey = StringUtils.defaultIfBlank(props.get(MODULE_KEY_PROPERTY), props.get(CoreProperties.PROJECT_KEY_PROPERTY));
+ if (missing.length() != 0) {
+ throw MessageException.of("You must define the following mandatory properties for '" + (moduleKey == null ? "Unknown" : moduleKey) + "': " + missing);
+ }
+ }
+
+ protected static void validateDirectories(Map<String, String> props, File baseDir, String projectId) {
+ if (!props.containsKey(PROPERTY_MODULES)) {
+ // SONARPLUGINS-2285 Not an aggregator project so we can validate that paths are correct if defined
+
+ // Check sonar.tests
+ String[] testPaths = getListFromProperty(props, PROPERTY_TESTS);
+ checkExistenceOfPaths(projectId, baseDir, testPaths, PROPERTY_TESTS);
+ }
+ }
+
+ @VisibleForTesting
+ protected static void cleanAndCheckProjectDefinitions(ProjectDefinition project) {
+ if (project.getSubProjects().isEmpty()) {
+ cleanAndCheckModuleProperties(project);
+ } else {
+ cleanAndCheckAggregatorProjectProperties(project);
+
+ // clean modules properties as well
+ for (ProjectDefinition module : project.getSubProjects()) {
+ cleanAndCheckProjectDefinitions(module);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ protected static void cleanAndCheckModuleProperties(ProjectDefinition project) {
+ Map<String, String> properties = project.properties();
+
+ // We need to check the existence of source directories
+ String[] sourcePaths = getListFromProperty(properties, PROPERTY_SOURCES);
+ checkExistenceOfPaths(project.getKey(), project.getBaseDir(), sourcePaths, PROPERTY_SOURCES);
+ }
+
+ @VisibleForTesting
+ protected static void cleanAndCheckAggregatorProjectProperties(ProjectDefinition project) {
+ Map<String, String> properties = project.properties();
+
+ // SONARPLUGINS-2295
+ String[] sourceDirs = getListFromProperty(properties, PROPERTY_SOURCES);
+ for (String path : sourceDirs) {
+ File sourceFolder = resolvePath(project.getBaseDir(), path);
+ if (sourceFolder.isDirectory()) {
+ LOG.warn("/!\\ A multi-module project can't have source folders, so '{}' won't be used for the analysis. " +
+ "If you want to analyse files of this folder, you should create another sub-module and move them inside it.",
+ sourceFolder.toString());
+ }
+ }
+
+ // "aggregator" project must not have the following properties:
+ properties.remove(PROPERTY_SOURCES);
+ properties.remove(PROPERTY_TESTS);
+ }
+
+ @VisibleForTesting
+ protected static void mergeParentProperties(Map<String, String> childProps, Map<String, String> parentProps) {
+ for (Map.Entry<String, String> entry : parentProps.entrySet()) {
+ String key = entry.getKey();
+ if ((!childProps.containsKey(key) || childProps.get(key).equals(entry.getValue()))
+ && !NON_HERITED_PROPERTIES_FOR_CHILD.contains(key)) {
+ childProps.put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ @VisibleForTesting
+ protected static void checkExistenceOfPaths(String moduleRef, File baseDir, String[] paths, String propName) {
+ for (String path : paths) {
+ File sourceFolder = resolvePath(baseDir, path);
+ if (!sourceFolder.exists()) {
+ LOG.error(MessageFormat.format(INVALID_VALUE_OF_X_FOR_Y, propName, moduleRef));
+ throw MessageException.of("The folder '" + path + "' does not exist for '" + moduleRef +
+ "' (base directory = " + baseDir.getAbsolutePath() + ")");
+ }
+ }
+ }
+
+ protected static File resolvePath(File baseDir, String path) {
+ Path filePath = Paths.get(path);
+ if (!filePath.isAbsolute()) {
+ filePath = baseDir.toPath().resolve(path);
+ }
+ return filePath.normalize().toFile();
+ }
+
+ /**
+ * Transforms a comma-separated list String property in to a array of trimmed strings.
+ *
+ * This works even if they are separated by whitespace characters (space char, EOL, ...)
+ *
+ */
+ static String[] getListFromProperty(Map<String, String> properties, String key) {
+ return (String[]) ObjectUtils.defaultIfNull(StringUtils.stripAll(StringUtils.split(properties.get(key), ',')), new String[0]);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorValidator.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorValidator.java
new file mode 100644
index 00000000000..7e36ab54958
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectReactorValidator.java
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.sonar.api.utils.MessageException;
+
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import com.google.common.base.Joiner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.config.Settings;
+import org.sonar.core.component.ComponentKeys;
+
+/**
+ * This class aims at validating project reactor
+ * @since 3.6
+ */
+public class ProjectReactorValidator {
+
+ private static final String SONAR_PHASE = "sonar.phase";
+ private final Settings settings;
+ private final DefaultAnalysisMode mode;
+
+ public ProjectReactorValidator(Settings settings, DefaultAnalysisMode mode) {
+ this.settings = settings;
+ this.mode = mode;
+ }
+
+ public void validate(ProjectReactor reactor) {
+ String branch = reactor.getRoot().getBranch();
+
+ List<String> validationMessages = new ArrayList<>();
+ checkDeprecatedProperties(validationMessages);
+
+ for (ProjectDefinition moduleDef : reactor.getProjects()) {
+ if (mode.isIssues()) {
+ validateModuleIssuesMode(moduleDef, validationMessages);
+ } else {
+ validateModule(moduleDef, validationMessages);
+ }
+ }
+
+ validateBranch(validationMessages, branch);
+
+ if (!validationMessages.isEmpty()) {
+ throw MessageException.of("Validation of project reactor failed:\n o " + Joiner.on("\n o ").join(validationMessages));
+ }
+ }
+
+ private static void validateModuleIssuesMode(ProjectDefinition moduleDef, List<String> validationMessages) {
+ if (!ComponentKeys.isValidModuleKeyIssuesMode(moduleDef.getKey())) {
+ validationMessages.add(String.format("\"%s\" is not a valid project or module key. "
+ + "Allowed characters in issues mode are alphanumeric, '-', '_', '.', '/' and ':', with at least one non-digit.", moduleDef.getKey()));
+ }
+ }
+
+ private static void validateModule(ProjectDefinition moduleDef, List<String> validationMessages) {
+ if (!ComponentKeys.isValidModuleKey(moduleDef.getKey())) {
+ validationMessages.add(String.format("\"%s\" is not a valid project or module key. "
+ + "Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.", moduleDef.getKey()));
+ }
+ }
+
+ private void checkDeprecatedProperties(List<String> validationMessages) {
+ if (settings.getString(SONAR_PHASE) != null) {
+ validationMessages.add(String.format("Property \"%s\" is deprecated. Please remove it from your configuration.", SONAR_PHASE));
+ }
+ }
+
+ private static void validateBranch(List<String> validationMessages, @Nullable String branch) {
+ if (StringUtils.isNotEmpty(branch) && !ComponentKeys.isValidBranch(branch)) {
+ validationMessages.add(String.format("\"%s\" is not a valid branch name. "
+ + "Allowed characters are alphanumeric, '-', '_', '.' and '/'.", branch));
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java
new file mode 100644
index 00000000000..ff8108e4871
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java
@@ -0,0 +1,277 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.DefaultFileLinesContextFactory;
+import org.sonar.batch.DefaultProjectTree;
+import org.sonar.batch.ProjectConfigurator;
+import org.sonar.batch.analysis.AnalysisProperties;
+import org.sonar.batch.analysis.AnalysisTempFolderProvider;
+import org.sonar.batch.analysis.AnalysisWSLoaderProvider;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.ExtensionInstaller;
+import org.sonar.batch.bootstrap.ExtensionMatcher;
+import org.sonar.batch.bootstrap.ExtensionUtils;
+import org.sonar.batch.bootstrap.MetricProvider;
+import org.sonar.batch.cache.ProjectPersistentCacheProvider;
+import org.sonar.batch.cpd.CpdExecutor;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.batch.events.EventBus;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.index.Caches;
+import org.sonar.batch.index.DefaultIndex;
+import org.sonar.batch.issue.DefaultIssueCallback;
+import org.sonar.batch.issue.DefaultProjectIssues;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.issue.tracking.DefaultServerLineHashesLoader;
+import org.sonar.batch.issue.tracking.IssueTransition;
+import org.sonar.batch.issue.tracking.LocalIssueTracking;
+import org.sonar.batch.issue.tracking.ServerIssueRepository;
+import org.sonar.batch.issue.tracking.ServerLineHashesLoader;
+import org.sonar.batch.mediumtest.ScanTaskObservers;
+import org.sonar.batch.phases.PhasesTimeProfiler;
+import org.sonar.batch.profiling.PhasesSumUpTimeProfiler;
+import org.sonar.batch.report.ActiveRulesPublisher;
+import org.sonar.batch.report.AnalysisContextReportPublisher;
+import org.sonar.batch.report.ComponentsPublisher;
+import org.sonar.batch.report.CoveragePublisher;
+import org.sonar.batch.report.MeasuresPublisher;
+import org.sonar.batch.report.MetadataPublisher;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.report.SourcePublisher;
+import org.sonar.batch.report.TestExecutionAndCoveragePublisher;
+import org.sonar.batch.repository.DefaultProjectRepositoriesLoader;
+import org.sonar.batch.repository.DefaultQualityProfileLoader;
+import org.sonar.batch.repository.DefaultServerIssuesLoader;
+import org.sonar.batch.repository.ProjectRepositories;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
+import org.sonar.batch.repository.ProjectRepositoriesProvider;
+import org.sonar.batch.repository.QualityProfileLoader;
+import org.sonar.batch.repository.QualityProfileProvider;
+import org.sonar.batch.repository.ServerIssuesLoader;
+import org.sonar.batch.repository.language.DefaultLanguagesRepository;
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.batch.rule.ActiveRulesLoader;
+import org.sonar.batch.rule.ActiveRulesProvider;
+import org.sonar.batch.rule.DefaultActiveRulesLoader;
+import org.sonar.batch.rule.DefaultRulesLoader;
+import org.sonar.batch.rule.RulesLoader;
+import org.sonar.batch.rule.RulesProvider;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+import org.sonar.batch.scan.measure.DefaultMetricFinder;
+import org.sonar.batch.scan.measure.DeprecatedMetricFinder;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.batch.source.CodeColorizers;
+import org.sonar.batch.test.TestPlanBuilder;
+import org.sonar.batch.test.TestableBuilder;
+import org.sonar.core.metric.BatchMetrics;
+import org.sonar.core.platform.ComponentContainer;
+
+public class ProjectScanContainer extends ComponentContainer {
+
+ private static final Logger LOG = Loggers.get(ProjectScanContainer.class);
+
+ private final AnalysisProperties props;
+ private ProjectLock lock;
+
+ public ProjectScanContainer(ComponentContainer globalContainer, AnalysisProperties props) {
+ super(globalContainer);
+ this.props = props;
+ }
+
+ @Override
+ protected void doBeforeStart() {
+ addBatchComponents();
+ lock = getComponentByType(ProjectLock.class);
+ lock.tryLock();
+ getComponentByType(WorkDirectoryCleaner.class).execute();
+ addBatchExtensions();
+ Settings settings = getComponentByType(Settings.class);
+ if (settings != null && settings.getBoolean(CoreProperties.PROFILING_LOG_PROPERTY)) {
+ add(PhasesSumUpTimeProfiler.class);
+ }
+ if (isTherePreviousAnalysis()) {
+ addIssueTrackingComponents();
+ }
+ }
+
+ @Override
+ public ComponentContainer startComponents() {
+ try {
+ return super.startComponents();
+ } catch (Exception e) {
+ // ensure that lock is released
+ if (lock != null) {
+ lock.stop();
+ }
+ throw e;
+ }
+ }
+
+ private void addBatchComponents() {
+ add(
+ props,
+ DefaultAnalysisMode.class,
+ ProjectReactorBuilder.class,
+ WorkDirectoryCleaner.class,
+ new MutableProjectReactorProvider(),
+ new ImmutableProjectReactorProvider(),
+ ProjectBuildersExecutor.class,
+ ProjectLock.class,
+ EventBus.class,
+ PhasesTimeProfiler.class,
+ ResourceTypes.class,
+ DefaultProjectTree.class,
+ ProjectExclusions.class,
+ ProjectReactorValidator.class,
+ new AnalysisWSLoaderProvider(),
+ CodeColorizers.class,
+ MetricProvider.class,
+ ProjectConfigurator.class,
+ DefaultIndex.class,
+ DefaultFileLinesContextFactory.class,
+ Caches.class,
+ BatchComponentCache.class,
+ DefaultIssueCallback.class,
+ new RulesProvider(),
+ new ProjectRepositoriesProvider(),
+ new ProjectPersistentCacheProvider(),
+
+ // temp
+ new AnalysisTempFolderProvider(),
+
+ // file system
+ InputPathCache.class,
+ PathResolver.class,
+
+ // rules
+ new ActiveRulesProvider(),
+ new QualityProfileProvider(),
+
+ // issues
+ IssueCache.class,
+ DefaultProjectIssues.class,
+ IssueTransition.class,
+
+ // metrics
+ DefaultMetricFinder.class,
+ DeprecatedMetricFinder.class,
+
+ // tests
+ TestPlanBuilder.class,
+ TestableBuilder.class,
+
+ // lang
+ Languages.class,
+ DefaultLanguagesRepository.class,
+
+ // Measures
+ MeasureCache.class,
+
+ ProjectSettings.class,
+
+ // Report
+ BatchMetrics.class,
+ ReportPublisher.class,
+ AnalysisContextReportPublisher.class,
+ MetadataPublisher.class,
+ ActiveRulesPublisher.class,
+ ComponentsPublisher.class,
+ MeasuresPublisher.class,
+ CoveragePublisher.class,
+ SourcePublisher.class,
+ TestExecutionAndCoveragePublisher.class,
+
+ // Cpd
+ CpdExecutor.class,
+ SonarCpdBlockIndex.class,
+
+ ScanTaskObservers.class,
+ UserRepositoryLoader.class);
+
+ addIfMissing(DefaultRulesLoader.class, RulesLoader.class);
+ addIfMissing(DefaultActiveRulesLoader.class, ActiveRulesLoader.class);
+ addIfMissing(DefaultQualityProfileLoader.class, QualityProfileLoader.class);
+ addIfMissing(DefaultProjectRepositoriesLoader.class, ProjectRepositoriesLoader.class);
+ }
+
+ private void addIssueTrackingComponents() {
+ add(
+ LocalIssueTracking.class,
+ ServerIssueRepository.class);
+ addIfMissing(DefaultServerIssuesLoader.class, ServerIssuesLoader.class);
+ addIfMissing(DefaultServerLineHashesLoader.class, ServerLineHashesLoader.class);
+ }
+
+ private boolean isTherePreviousAnalysis() {
+ if (getComponentByType(DefaultAnalysisMode.class).isNotAssociated()) {
+ return false;
+ }
+
+ return getComponentByType(ProjectRepositories.class).lastAnalysisDate() != null;
+ }
+
+ private void addBatchExtensions() {
+ getComponentByType(ExtensionInstaller.class).install(this, new BatchExtensionFilter());
+ }
+
+ @Override
+ protected void doAfterStart() {
+ DefaultAnalysisMode analysisMode = getComponentByType(DefaultAnalysisMode.class);
+ analysisMode.printMode();
+ LOG.debug("Start recursive analysis of project modules");
+ DefaultProjectTree tree = getComponentByType(DefaultProjectTree.class);
+ scanRecursively(tree.getRootProject());
+ if (analysisMode.isMediumTest()) {
+ getComponentByType(ScanTaskObservers.class).notifyEndOfScanTask();
+ }
+ }
+
+ private void scanRecursively(Project module) {
+ for (Project subModules : module.getModules()) {
+ scanRecursively(subModules);
+ }
+ scan(module);
+ }
+
+ @VisibleForTesting
+ void scan(Project module) {
+ new ModuleScanContainer(this, module).execute();
+ }
+
+ static class BatchExtensionFilter implements ExtensionMatcher {
+ @Override
+ public boolean accept(Object extension) {
+ return ExtensionUtils.isBatchSide(extension)
+ && ExtensionUtils.isInstantiationStrategy(extension, InstantiationStrategy.PER_BATCH);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectSettings.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectSettings.java
new file mode 100644
index 00000000000..382d02424a3
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/ProjectSettings.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.GlobalSettings;
+import org.sonar.batch.repository.ProjectRepositories;
+
+public class ProjectSettings extends Settings {
+
+ private final GlobalSettings globalSettings;
+ private final ProjectRepositories projectRepositories;
+ private final DefaultAnalysisMode mode;
+
+ public ProjectSettings(ProjectReactor reactor, GlobalSettings globalSettings, PropertyDefinitions propertyDefinitions,
+ ProjectRepositories projectRepositories, DefaultAnalysisMode mode) {
+ super(propertyDefinitions);
+ this.mode = mode;
+ getEncryption().setPathToSecretKey(globalSettings.getString(CoreProperties.ENCRYPTION_SECRET_KEY_PATH));
+ this.globalSettings = globalSettings;
+ this.projectRepositories = projectRepositories;
+ init(reactor);
+ }
+
+ private void init(ProjectReactor reactor) {
+ addProperties(globalSettings.getProperties());
+
+ addProperties(projectRepositories.settings(reactor.getRoot().getKeyWithBranch()));
+
+ addProperties(reactor.getRoot().properties());
+ }
+
+ @Override
+ protected void doOnGetProperties(String key) {
+ if (mode.isIssues() && key.endsWith(".secured") && !key.contains(".license")) {
+ throw MessageException.of("Access to the secured property '" + key
+ + "' is not possible in issues mode. The SonarQube plugin which requires this property must be deactivated in issues mode.");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/WorkDirectoryCleaner.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/WorkDirectoryCleaner.java
new file mode 100644
index 00000000000..b4857bf6afb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/WorkDirectoryCleaner.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.core.util.FileUtils;
+import org.sonar.home.cache.DirectoryLock;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+public class WorkDirectoryCleaner {
+ private Path workDir;
+
+ public WorkDirectoryCleaner(ProjectReactor projectReactor) {
+ workDir = projectReactor.getRoot().getWorkDir().toPath();
+ }
+
+ public void execute() {
+ if (!Files.exists(workDir)) {
+ return;
+ }
+
+ try (DirectoryStream<Path> stream = list()) {
+
+ Iterator<Path> it = stream.iterator();
+ while (it.hasNext()) {
+ FileUtils.deleteQuietly(it.next().toFile());
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to clean working directory: " + workDir.toString(), e);
+ }
+ }
+
+ private DirectoryStream<Path> list() throws IOException {
+ return Files.newDirectoryStream(workDir, new DirectoryStream.Filter<Path>() {
+ @Override
+ public boolean accept(Path entry) throws IOException {
+ return !DirectoryLock.LOCK_FILE_NAME.equals(entry.getFileName().toString());
+ }
+ });
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicates.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicates.java
new file mode 100644
index 00000000000..b68b6a3625a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicates.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.AbstractFilePredicate;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+
+/**
+ * Additional {@link org.sonar.api.batch.fs.FilePredicate}s that are
+ * not published in public API
+ */
+class AdditionalFilePredicates {
+
+ private AdditionalFilePredicates() {
+ // only static inner classes
+ }
+
+ static class KeyPredicate extends AbstractFilePredicate {
+ private final String key;
+
+ KeyPredicate(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return key.equals(((DefaultInputFile) f).key());
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ComponentIndexer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ComponentIndexer.java
new file mode 100644
index 00000000000..761f6d15823
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ComponentIndexer.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+
+/**
+ * Index all files/directories of the module in SQ database and importing source code.
+ *
+ * @since 4.2
+ */
+@BatchSide
+public class ComponentIndexer {
+
+ private final Languages languages;
+ private final SonarIndex sonarIndex;
+ private final Project module;
+ private final BatchComponentCache componentCache;
+
+ public ComponentIndexer(Project module, Languages languages, SonarIndex sonarIndex, BatchComponentCache componentCache) {
+ this.module = module;
+ this.languages = languages;
+ this.sonarIndex = sonarIndex;
+ this.componentCache = componentCache;
+ }
+
+ public void execute(DefaultModuleFileSystem fs) {
+ module.setBaseDir(fs.baseDir());
+
+ for (InputFile inputFile : fs.inputFiles()) {
+ String languageKey = inputFile.language();
+ boolean unitTest = InputFile.Type.TEST == inputFile.type();
+ Resource sonarFile = File.create(inputFile.relativePath(), languages.get(languageKey), unitTest);
+ sonarIndex.index(sonarFile);
+ BatchComponent file = componentCache.get(sonarFile);
+ file.setInputComponent(inputFile);
+ Resource sonarDir = file.parent().resource();
+ InputDir inputDir = fs.inputDir(inputFile.file().getParentFile());
+ componentCache.get(sonarDir).setInputComponent(inputDir);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java
new file mode 100644
index 00000000000..125a73d0078
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java
@@ -0,0 +1,285 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.fs.InputFile.Status;
+
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import java.io.File;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.api.scan.filesystem.FileQuery;
+import org.sonar.api.scan.filesystem.ModuleFileSystem;
+import org.sonar.api.utils.MessageException;
+
+/**
+ * @since 3.5
+ */
+public class DefaultModuleFileSystem extends DefaultFileSystem implements ModuleFileSystem {
+
+ private String moduleKey;
+ private FileIndexer indexer;
+ private Settings settings;
+
+ private File buildDir;
+ private List<File> sourceDirsOrFiles = Lists.newArrayList();
+ private List<File> testDirsOrFiles = Lists.newArrayList();
+ private List<File> binaryDirs = Lists.newArrayList();
+ private ComponentIndexer componentIndexer;
+ private boolean initialized;
+
+ public DefaultModuleFileSystem(ModuleInputFileCache moduleInputFileCache, Project project,
+ Settings settings, FileIndexer indexer, ModuleFileSystemInitializer initializer, ComponentIndexer componentIndexer, DefaultAnalysisMode mode) {
+ super(initializer.baseDir(), moduleInputFileCache);
+ setFields(project, settings, indexer, initializer, componentIndexer, mode);
+ }
+
+ @VisibleForTesting
+ public DefaultModuleFileSystem(Project project,
+ Settings settings, FileIndexer indexer, ModuleFileSystemInitializer initializer, ComponentIndexer componentIndexer, DefaultAnalysisMode mode) {
+ super(initializer.baseDir().toPath());
+ setFields(project, settings, indexer, initializer, componentIndexer, mode);
+ }
+
+ private void setFields(Project project,
+ Settings settings, FileIndexer indexer, ModuleFileSystemInitializer initializer, ComponentIndexer componentIndexer, DefaultAnalysisMode mode) {
+ this.componentIndexer = componentIndexer;
+ this.moduleKey = project.getKey();
+ this.settings = settings;
+ this.indexer = indexer;
+ setWorkDir(initializer.workingDir());
+ this.buildDir = initializer.buildDir();
+ this.sourceDirsOrFiles = initializer.sources();
+ this.testDirsOrFiles = initializer.tests();
+ this.binaryDirs = initializer.binaryDirs();
+
+ // filter the files sensors have access to
+ if (!mode.scanAllFiles()) {
+ setDefaultPredicate(predicates.not(predicates.hasStatus(Status.SAME)));
+ }
+ }
+
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ public String moduleKey() {
+ return moduleKey;
+ }
+
+ @Override
+ @CheckForNull
+ public File buildDir() {
+ return buildDir;
+ }
+
+ @Override
+ public List<File> sourceDirs() {
+ return keepOnlyDirs(sourceDirsOrFiles);
+ }
+
+ public List<File> sources() {
+ return sourceDirsOrFiles;
+ }
+
+ @Override
+ public List<File> testDirs() {
+ return keepOnlyDirs(testDirsOrFiles);
+ }
+
+ public List<File> tests() {
+ return testDirsOrFiles;
+ }
+
+ private static List<File> keepOnlyDirs(List<File> dirsOrFiles) {
+ List<File> result = new ArrayList<>();
+ for (File f : dirsOrFiles) {
+ if (f.isDirectory()) {
+ result.add(f);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<File> binaryDirs() {
+ return binaryDirs;
+ }
+
+ @Override
+ public Charset encoding() {
+ final Charset charset;
+ String encoding = settings.getString(CoreProperties.ENCODING_PROPERTY);
+ if (StringUtils.isNotEmpty(encoding)) {
+ charset = Charset.forName(StringUtils.trim(encoding));
+ } else {
+ charset = Charset.defaultCharset();
+ }
+ return charset;
+ }
+
+ @Override
+ public boolean isDefaultJvmEncoding() {
+ return !settings.hasKey(CoreProperties.ENCODING_PROPERTY);
+ }
+
+ /**
+ * Should not be used - only for old plugins
+ *
+ * @deprecated since 4.0
+ */
+ @Deprecated
+ void addSourceDir(File dir) {
+ throw modificationNotPermitted();
+ }
+
+ /**
+ * Should not be used - only for old plugins
+ *
+ * @deprecated since 4.0
+ */
+ @Deprecated
+ void addTestDir(File dir) {
+ throw modificationNotPermitted();
+ }
+
+ private static UnsupportedOperationException modificationNotPermitted() {
+ return new UnsupportedOperationException("Modifications of the file system are not permitted");
+ }
+
+ /**
+ * @return
+ * @deprecated in 4.2. Replaced by {@link #encoding()}
+ */
+ @Override
+ @Deprecated
+ public Charset sourceCharset() {
+ return encoding();
+ }
+
+ /**
+ * @deprecated in 4.2. Replaced by {@link #workDir()}
+ */
+ @Deprecated
+ @Override
+ public File workingDir() {
+ return workDir();
+ }
+
+ @Override
+ public List<File> files(FileQuery query) {
+ doPreloadFiles();
+ Collection<FilePredicate> predicates = Lists.newArrayList();
+ for (Map.Entry<String, Collection<String>> entry : query.attributes().entrySet()) {
+ predicates.add(fromDeprecatedAttribute(entry.getKey(), entry.getValue()));
+ }
+ return ImmutableList.copyOf(files(predicates().and(predicates)));
+ }
+
+ @Override
+ protected void doPreloadFiles() {
+ if (!initialized) {
+ throw MessageException.of("Accessing the filesystem before the Sensor phase is not supported. Please update your plugin.");
+ }
+ }
+
+ public void index() {
+ if (initialized) {
+ throw MessageException.of("Module filesystem can only be indexed once");
+ }
+ initialized = true;
+ indexer.index(this);
+ if (componentIndexer != null) {
+ componentIndexer.execute(this);
+ }
+ }
+
+ private FilePredicate fromDeprecatedAttribute(String key, Collection<String> value) {
+ if ("TYPE".equals(key)) {
+ return predicates().or(Collections2.transform(value, new Function<String, FilePredicate>() {
+ @Override
+ public FilePredicate apply(@Nullable String s) {
+ return s == null ? predicates().all() : predicates().hasType(org.sonar.api.batch.fs.InputFile.Type.valueOf(s));
+ }
+ }));
+ }
+ if ("STATUS".equals(key)) {
+ return predicates().or(Collections2.transform(value, new Function<String, FilePredicate>() {
+ @Override
+ public FilePredicate apply(@Nullable String s) {
+ return s == null ? predicates().all() : predicates().hasStatus(org.sonar.api.batch.fs.InputFile.Status.valueOf(s));
+ }
+ }));
+ }
+ if ("LANG".equals(key)) {
+ return predicates().or(Collections2.transform(value, new Function<String, FilePredicate>() {
+ @Override
+ public FilePredicate apply(@Nullable String s) {
+ return s == null ? predicates().all() : predicates().hasLanguage(s);
+ }
+ }));
+ }
+ if ("CMP_KEY".equals(key)) {
+ return predicates().or(Collections2.transform(value, new Function<String, FilePredicate>() {
+ @Override
+ public FilePredicate apply(@Nullable String s) {
+ return s == null ? predicates().all() : new AdditionalFilePredicates.KeyPredicate(s);
+ }
+ }));
+ }
+ throw new IllegalArgumentException("Unsupported file attribute: " + key);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ DefaultModuleFileSystem that = (DefaultModuleFileSystem) o;
+ return moduleKey.equals(that.moduleKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return moduleKey.hashCode();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DeprecatedFileFilters.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DeprecatedFileFilters.java
new file mode 100644
index 00000000000..29968e7e102
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/DeprecatedFileFilters.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFileFilter;
+import org.sonar.api.scan.filesystem.FileSystemFilter;
+import org.sonar.api.scan.filesystem.FileType;
+import org.sonar.api.scan.filesystem.ModuleFileSystem;
+
+public class DeprecatedFileFilters implements InputFileFilter {
+ private final FileSystemFilter[] filters;
+
+ public DeprecatedFileFilters(FileSystemFilter[] filters) {
+ this.filters = filters;
+ }
+
+ public DeprecatedFileFilters() {
+ this(new FileSystemFilter[0]);
+ }
+
+ @Override
+ public boolean accept(InputFile inputFile) {
+ if (filters.length > 0) {
+ DeprecatedContext context = new DeprecatedContext(inputFile);
+ for (FileSystemFilter filter : filters) {
+ if (!filter.accept(inputFile.file(), context)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ static class DeprecatedContext implements FileSystemFilter.Context {
+ private final InputFile inputFile;
+
+ DeprecatedContext(InputFile inputFile) {
+ this.inputFile = inputFile;
+ }
+
+ @Override
+ public ModuleFileSystem fileSystem() {
+ throw new UnsupportedOperationException("Not supported since 4.0");
+ }
+
+ @Override
+ public FileType type() {
+ String type = inputFile.type().name();
+ return FileType.valueOf(type);
+ }
+
+ @Override
+ public String relativePath() {
+ return inputFile.relativePath();
+ }
+
+ @Override
+ public String canonicalPath() {
+ return inputFile.absolutePath();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ExclusionFilters.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ExclusionFilters.java
new file mode 100644
index 00000000000..2a2952884f5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ExclusionFilters.java
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.PathPattern;
+import org.sonar.api.scan.filesystem.FileExclusions;
+
+@BatchSide
+public class ExclusionFilters {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ExclusionFilters.class);
+
+ private final FileExclusions exclusionSettings;
+
+ private PathPattern[] mainInclusions;
+ private PathPattern[] mainExclusions;
+ private PathPattern[] testInclusions;
+ private PathPattern[] testExclusions;
+
+ public ExclusionFilters(FileExclusions exclusions) {
+ this.exclusionSettings = exclusions;
+ }
+
+ public void prepare() {
+ mainInclusions = prepareMainInclusions();
+ mainExclusions = prepareMainExclusions();
+ testInclusions = prepareTestInclusions();
+ testExclusions = prepareTestExclusions();
+ log("Included sources: ", mainInclusions);
+ log("Excluded sources: ", mainExclusions);
+ log("Included tests: ", testInclusions);
+ log("Excluded tests: ", testExclusions);
+ }
+
+ public boolean hasPattern() {
+ return mainInclusions.length > 0 || mainExclusions.length > 0 || testInclusions.length > 0 || testExclusions.length > 0;
+ }
+
+ private void log(String title, PathPattern[] patterns) {
+ if (patterns.length > 0) {
+ LOG.info(title);
+ for (PathPattern pattern : patterns) {
+ LOG.info(" " + pattern);
+ }
+ }
+ }
+
+ public boolean accept(InputFile inputFile, InputFile.Type type) {
+ PathPattern[] inclusionPatterns;
+ PathPattern[] exclusionPatterns;
+ if (InputFile.Type.MAIN == type) {
+ inclusionPatterns = mainInclusions;
+ exclusionPatterns = mainExclusions;
+ } else if (InputFile.Type.TEST == type) {
+ inclusionPatterns = testInclusions;
+ exclusionPatterns = testExclusions;
+ } else {
+ throw new IllegalArgumentException("Unknown file type: " + type);
+ }
+
+ if (inclusionPatterns.length > 0) {
+ boolean matchInclusion = false;
+ for (PathPattern pattern : inclusionPatterns) {
+ matchInclusion |= pattern.match(inputFile);
+ }
+ if (!matchInclusion) {
+ return false;
+ }
+ }
+ if (exclusionPatterns.length > 0) {
+ for (PathPattern pattern : exclusionPatterns) {
+ if (pattern.match(inputFile)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ PathPattern[] prepareMainInclusions() {
+ if (exclusionSettings.sourceInclusions().length > 0) {
+ // User defined params
+ return PathPattern.create(exclusionSettings.sourceInclusions());
+ }
+ return new PathPattern[0];
+ }
+
+ PathPattern[] prepareTestInclusions() {
+ return PathPattern.create(computeTestInclusions());
+ }
+
+ private String[] computeTestInclusions() {
+ if (exclusionSettings.testInclusions().length > 0) {
+ // User defined params
+ return exclusionSettings.testInclusions();
+ }
+ return ArrayUtils.EMPTY_STRING_ARRAY;
+ }
+
+ PathPattern[] prepareMainExclusions() {
+ String[] patterns = (String[]) ArrayUtils.addAll(
+ exclusionSettings.sourceExclusions(), computeTestInclusions());
+ return PathPattern.create(patterns);
+ }
+
+ PathPattern[] prepareTestExclusions() {
+ return PathPattern.create(exclusionSettings.testExclusions());
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileIndexer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileIndexer.java
new file mode 100644
index 00000000000..9ebf91886a0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileIndexer.java
@@ -0,0 +1,268 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.InputFileFilter;
+import org.sonar.api.batch.fs.internal.DefaultInputDir;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.util.ProgressReport;
+import org.sonar.home.cache.DirectoryLock;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystemLoopException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Index input files into {@link InputPathCache}.
+ */
+@BatchSide
+public class FileIndexer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FileIndexer.class);
+ private final List<InputFileFilter> filters;
+ private final boolean isAggregator;
+ private final ExclusionFilters exclusionFilters;
+ private final InputFileBuilderFactory inputFileBuilderFactory;
+
+ private ProgressReport progressReport;
+ private ExecutorService executorService;
+ private List<Future<Void>> tasks;
+
+ public FileIndexer(List<InputFileFilter> filters, ExclusionFilters exclusionFilters, InputFileBuilderFactory inputFileBuilderFactory,
+ ProjectDefinition def) {
+ this.filters = filters;
+ this.exclusionFilters = exclusionFilters;
+ this.inputFileBuilderFactory = inputFileBuilderFactory;
+ this.isAggregator = !def.getSubProjects().isEmpty();
+ }
+
+ void index(DefaultModuleFileSystem fileSystem) {
+ if (isAggregator) {
+ // No indexing for an aggregator module
+ return;
+ }
+ progressReport = new ProgressReport("Report about progress of file indexation", TimeUnit.SECONDS.toMillis(10));
+ progressReport.start("Index files");
+ exclusionFilters.prepare();
+
+ Progress progress = new Progress();
+
+ InputFileBuilder inputFileBuilder = inputFileBuilderFactory.create(fileSystem);
+ int threads = Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
+ executorService = Executors.newFixedThreadPool(threads, new ThreadFactoryBuilder().setNameFormat("FileIndexer-%d").build());
+ tasks = new ArrayList<>();
+ indexFiles(fileSystem, progress, inputFileBuilder, fileSystem.sources(), InputFile.Type.MAIN);
+ indexFiles(fileSystem, progress, inputFileBuilder, fileSystem.tests(), InputFile.Type.TEST);
+
+ waitForTasksToComplete();
+
+ progressReport.stop(progress.count() + " files indexed");
+
+ if (exclusionFilters.hasPattern()) {
+ LOG.info(progress.excludedByPatternsCount() + " files ignored because of inclusion/exclusion patterns");
+ }
+ }
+
+ private void waitForTasksToComplete() {
+ executorService.shutdown();
+ for (Future<Void> task : tasks) {
+ try {
+ task.get();
+ } catch (ExecutionException e) {
+ // Unwrap ExecutionException
+ throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause() : new IllegalStateException(e.getCause());
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ private void indexFiles(DefaultModuleFileSystem fileSystem, Progress progress, InputFileBuilder inputFileBuilder, List<File> sources, InputFile.Type type) {
+ try {
+ for (File dirOrFile : sources) {
+ if (dirOrFile.isDirectory()) {
+ indexDirectory(inputFileBuilder, fileSystem, progress, dirOrFile, type);
+ } else {
+ indexFile(inputFileBuilder, fileSystem, progress, dirOrFile.toPath(), type);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to index files", e);
+ }
+ }
+
+ private void indexDirectory(final InputFileBuilder inputFileBuilder, final DefaultModuleFileSystem fileSystem, final Progress status,
+ final File dirToIndex, final InputFile.Type type) throws IOException {
+ Files.walkFileTree(dirToIndex.toPath().normalize(), Collections.singleton(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
+ new IndexFileVisitor(inputFileBuilder, fileSystem, status, type));
+ }
+
+ private void indexFile(InputFileBuilder inputFileBuilder, DefaultModuleFileSystem fileSystem, Progress progress, Path sourceFile, InputFile.Type type) throws IOException {
+ // get case of real file without resolving link
+ Path realFile = sourceFile.toRealPath(LinkOption.NOFOLLOW_LINKS);
+ DefaultInputFile inputFile = inputFileBuilder.create(realFile.toFile());
+ if (inputFile != null) {
+ // Set basedir on input file prior to adding it to the FS since exclusions filters may require the absolute path
+ inputFile.setModuleBaseDir(fileSystem.baseDirPath());
+ if (exclusionFilters.accept(inputFile, type)) {
+ indexFile(inputFileBuilder, fileSystem, progress, inputFile, type);
+ } else {
+ progress.increaseExcludedByPatternsCount();
+ }
+ }
+ }
+
+ private void indexFile(final InputFileBuilder inputFileBuilder, final DefaultModuleFileSystem fs,
+ final Progress status, final DefaultInputFile inputFile, final InputFile.Type type) {
+
+ tasks.add(executorService.submit(new Callable<Void>() {
+ @Override
+ public Void call() {
+ DefaultInputFile completedInputFile = inputFileBuilder.completeAndComputeMetadata(inputFile, type);
+ if (completedInputFile != null && accept(completedInputFile)) {
+ fs.add(completedInputFile);
+ status.markAsIndexed(completedInputFile);
+ File parentDir = completedInputFile.file().getParentFile();
+ String relativePath = new PathResolver().relativePath(fs.baseDir(), parentDir);
+ if (relativePath != null) {
+ DefaultInputDir inputDir = new DefaultInputDir(fs.moduleKey(), relativePath);
+ fs.add(inputDir);
+ }
+ }
+ return null;
+ }
+ }));
+
+ }
+
+ private boolean accept(InputFile inputFile) {
+ // InputFileFilter extensions
+ for (InputFileFilter filter : filters) {
+ if (!filter.accept(inputFile)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private class IndexFileVisitor implements FileVisitor<Path> {
+ private InputFileBuilder inputFileBuilder;
+ private DefaultModuleFileSystem fileSystem;
+ private Progress status;
+ private Type type;
+
+ IndexFileVisitor(InputFileBuilder inputFileBuilder, DefaultModuleFileSystem fileSystem, Progress status, InputFile.Type type) {
+ this.inputFileBuilder = inputFileBuilder;
+ this.fileSystem = fileSystem;
+ this.status = status;
+ this.type = type;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+ Path fileName = dir.getFileName();
+
+ if (fileName != null && fileName.toString().length() > 1 && fileName.toString().charAt(0) == '.') {
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ if (Files.isHidden(dir)) {
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (!Files.isHidden(file) && !DirectoryLock.LOCK_FILE_NAME.equals(file.getFileName().toString())) {
+ indexFile(inputFileBuilder, fileSystem, status, file, type);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+ if (exc != null && exc instanceof FileSystemLoopException) {
+ LOG.warn("Not indexing due to symlink loop: {}", file.toFile());
+ return FileVisitResult.CONTINUE;
+ }
+
+ throw exc;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+ }
+
+ private class Progress {
+ private final Set<Path> indexed = new HashSet<>();
+ private int excludedByPatternsCount = 0;
+
+ synchronized void markAsIndexed(InputFile inputFile) {
+ if (indexed.contains(inputFile.path())) {
+ throw MessageException.of("File " + inputFile + " can't be indexed twice. Please check that inclusion/exclusion patterns produce "
+ + "disjoint sets for main and test files");
+ }
+ indexed.add(inputFile.path());
+ progressReport.message(indexed.size() + " files indexed... (last one was " + inputFile.relativePath() + ")");
+ }
+
+ void increaseExcludedByPatternsCount() {
+ excludedByPatternsCount++;
+ }
+
+ public int excludedByPatternsCount() {
+ return excludedByPatternsCount;
+ }
+
+ int count() {
+ return indexed.size();
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileSystemLogger.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileSystemLogger.java
new file mode 100644
index 00000000000..0d717301283
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/FileSystemLogger.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.File;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.scan.filesystem.PathResolver;
+
+@BatchSide
+public class FileSystemLogger {
+
+ private final DefaultModuleFileSystem fs;
+
+ public FileSystemLogger(DefaultModuleFileSystem fs) {
+ this.fs = fs;
+ }
+
+ public void log() {
+ doLog(LoggerFactory.getLogger(getClass()));
+ }
+
+ @VisibleForTesting
+ void doLog(Logger logger) {
+ logDir(logger, "Base dir: ", fs.baseDir());
+ logDir(logger, "Working dir: ", fs.workDir());
+ logPaths(logger, "Source paths: ", fs.baseDir(), fs.sources());
+ logPaths(logger, "Test paths: ", fs.baseDir(), fs.tests());
+ logPaths(logger, "Binary dirs: ", fs.baseDir(), fs.binaryDirs());
+ logEncoding(logger, fs.encoding());
+ }
+
+ private void logEncoding(Logger logger, Charset charset) {
+ if (!fs.isDefaultJvmEncoding()) {
+ logger.info("Source encoding: " + charset.displayName() + ", default locale: " + Locale.getDefault());
+ } else {
+ logger.warn("Source encoding is platform dependent (" + charset.displayName() + "), default locale: " + Locale.getDefault());
+ }
+ }
+
+ private static void logPaths(Logger logger, String label, File baseDir, List<File> paths) {
+ if (!paths.isEmpty()) {
+ PathResolver resolver = new PathResolver();
+ StringBuilder sb = new StringBuilder(label);
+ for (Iterator<File> it = paths.iterator(); it.hasNext();) {
+ File file = it.next();
+ String relativePathToBaseDir = resolver.relativePath(baseDir, file);
+ if (relativePathToBaseDir == null) {
+ sb.append(file);
+ } else if (StringUtils.isBlank(relativePathToBaseDir)) {
+ sb.append(".");
+ } else {
+ sb.append(relativePathToBaseDir);
+ }
+ if (it.hasNext()) {
+ sb.append(", ");
+ }
+ }
+ logger.info(sb.toString());
+ }
+ }
+
+ private void logDir(Logger logger, String label, File dir) {
+ if (dir != null) {
+ logger.info(label + dir.getAbsolutePath());
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilder.java
new file mode 100644
index 00000000000..49cbd423cf9
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilder.java
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.config.Settings;
+import org.sonar.api.scan.filesystem.PathResolver;
+
+import javax.annotation.CheckForNull;
+
+import java.io.File;
+
+class InputFileBuilder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(InputFileBuilder.class);
+
+ private final String moduleKey;
+ private final PathResolver pathResolver;
+ private final LanguageDetection langDetection;
+ private final StatusDetection statusDetection;
+ private final DefaultModuleFileSystem fs;
+ private final Settings settings;
+ private final FileMetadata fileMetadata;
+
+ InputFileBuilder(String moduleKey, PathResolver pathResolver, LanguageDetection langDetection,
+ StatusDetection statusDetection, DefaultModuleFileSystem fs, Settings settings, FileMetadata fileMetadata) {
+ this.moduleKey = moduleKey;
+ this.pathResolver = pathResolver;
+ this.langDetection = langDetection;
+ this.statusDetection = statusDetection;
+ this.fs = fs;
+ this.settings = settings;
+ this.fileMetadata = fileMetadata;
+ }
+
+ String moduleKey() {
+ return moduleKey;
+ }
+
+ PathResolver pathResolver() {
+ return pathResolver;
+ }
+
+ LanguageDetection langDetection() {
+ return langDetection;
+ }
+
+ StatusDetection statusDetection() {
+ return statusDetection;
+ }
+
+ FileSystem fs() {
+ return fs;
+ }
+
+ @CheckForNull
+ DefaultInputFile create(File file) {
+ String relativePath = pathResolver.relativePath(fs.baseDir(), file);
+ if (relativePath == null) {
+ LOG.warn("File '{}' is ignored. It is not located in module basedir '{}'.", file.getAbsolutePath(), fs.baseDir());
+ return null;
+ }
+ return new DefaultInputFile(moduleKey, relativePath);
+ }
+
+ /**
+ * Optimization to not compute InputFile metadata if the file is excluded from analysis.
+ */
+ @CheckForNull
+ DefaultInputFile completeAndComputeMetadata(DefaultInputFile inputFile, InputFile.Type type) {
+ inputFile.setType(type);
+ inputFile.setModuleBaseDir(fs.baseDir().toPath());
+ inputFile.setCharset(fs.encoding());
+
+ String lang = langDetection.language(inputFile);
+ if (lang == null && !settings.getBoolean(CoreProperties.IMPORT_UNKNOWN_FILES_KEY)) {
+ return null;
+ }
+ inputFile.setLanguage(lang);
+
+ inputFile.initMetadata(fileMetadata.readMetadata(inputFile.file(), fs.encoding()));
+
+ inputFile.setStatus(statusDetection.status(inputFile.moduleKey(), inputFile.relativePath(), inputFile.hash()));
+
+ return inputFile;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactory.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactory.java
new file mode 100644
index 00000000000..46dc2b2fbcd
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactory.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.config.Settings;
+import org.sonar.api.scan.filesystem.PathResolver;
+
+@BatchSide
+public class InputFileBuilderFactory {
+
+ private final String moduleKey;
+ private final PathResolver pathResolver;
+ private final LanguageDetectionFactory langDetectionFactory;
+ private final StatusDetectionFactory statusDetectionFactory;
+ private final Settings settings;
+ private final FileMetadata fileMetadata;
+
+ public InputFileBuilderFactory(ProjectDefinition def, PathResolver pathResolver, LanguageDetectionFactory langDetectionFactory,
+ StatusDetectionFactory statusDetectionFactory, Settings settings, FileMetadata fileMetadata) {
+ this.fileMetadata = fileMetadata;
+ this.moduleKey = def.getKeyWithBranch();
+ this.pathResolver = pathResolver;
+ this.langDetectionFactory = langDetectionFactory;
+ this.statusDetectionFactory = statusDetectionFactory;
+ this.settings = settings;
+ }
+
+ InputFileBuilder create(DefaultModuleFileSystem fs) {
+ return new InputFileBuilder(moduleKey, pathResolver, langDetectionFactory.create(), statusDetectionFactory.create(), fs, settings, fileMetadata);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputPathCache.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputPathCache.java
new file mode 100644
index 00000000000..3375f92c7ff
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/InputPathCache.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import com.google.common.collect.Table;
+import com.google.common.collect.TreeBasedTable;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.batch.fs.InputFile;
+
+import javax.annotation.CheckForNull;
+
+/**
+ * Cache of all files and dirs. This cache is shared amongst all project modules. Inclusion and
+ * exclusion patterns are already applied.
+ */
+@BatchSide
+public class InputPathCache {
+
+ private final Table<String, String, InputFile> inputFileCache = TreeBasedTable.create();
+ private final Table<String, String, InputDir> inputDirCache = TreeBasedTable.create();
+
+ public Iterable<InputFile> allFiles() {
+ return inputFileCache.values();
+ }
+
+ public Iterable<InputDir> allDirs() {
+ return inputDirCache.values();
+ }
+
+ public Iterable<InputFile> filesByModule(String moduleKey) {
+ return inputFileCache.row(moduleKey).values();
+ }
+
+ public Iterable<InputDir> dirsByModule(String moduleKey) {
+ return inputDirCache.row(moduleKey).values();
+ }
+
+ public InputPathCache removeModule(String moduleKey) {
+ inputFileCache.row(moduleKey).clear();
+ inputDirCache.row(moduleKey).clear();
+ return this;
+ }
+
+ public InputPathCache remove(String moduleKey, InputFile inputFile) {
+ inputFileCache.remove(moduleKey, inputFile.relativePath());
+ return this;
+ }
+
+ public InputPathCache remove(String moduleKey, InputDir inputDir) {
+ inputDirCache.remove(moduleKey, inputDir.relativePath());
+ return this;
+ }
+
+ public InputPathCache put(String moduleKey, InputFile inputFile) {
+ inputFileCache.put(moduleKey, inputFile.relativePath(), inputFile);
+ return this;
+ }
+
+ public InputPathCache put(String moduleKey, InputDir inputDir) {
+ inputDirCache.put(moduleKey, inputDir.relativePath(), inputDir);
+ return this;
+ }
+
+ @CheckForNull
+ public InputFile getFile(String moduleKey, String relativePath) {
+ return inputFileCache.get(moduleKey, relativePath);
+ }
+
+ @CheckForNull
+ public InputDir getDir(String moduleKey, String relativePath) {
+ return inputDirCache.get(moduleKey, relativePath);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetection.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetection.java
new file mode 100644
index 00000000000..db1300e2a97
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetection.java
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.batch.repository.language.Language;
+import org.sonar.batch.repository.language.LanguagesRepository;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.PathPattern;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+
+import javax.annotation.CheckForNull;
+
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Detect language of a source file based on its suffix and configured patterns.
+ */
+class LanguageDetection {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LanguageDetection.class);
+
+ /**
+ * Lower-case extension -> languages
+ */
+ private final Map<String, PathPattern[]> patternsByLanguage = Maps.newLinkedHashMap();
+ private final List<String> languagesToConsider = Lists.newArrayList();
+ private final String forcedLanguage;
+
+ LanguageDetection(Settings settings, LanguagesRepository languages) {
+ for (Language language : languages.all()) {
+ String[] filePatterns = settings.getStringArray(getFileLangPatternPropKey(language.key()));
+ PathPattern[] pathPatterns = PathPattern.create(filePatterns);
+ if (pathPatterns.length > 0) {
+ patternsByLanguage.put(language.key(), pathPatterns);
+ } else {
+ // If no custom language pattern is defined then fallback to suffixes declared by language
+ String[] patterns = language.fileSuffixes().toArray(new String[language.fileSuffixes().size()]);
+ for (int i = 0; i < patterns.length; i++) {
+ String suffix = patterns[i];
+ String extension = sanitizeExtension(suffix);
+ patterns[i] = new StringBuilder().append("**/*.").append(extension).toString();
+ }
+ PathPattern[] defaultLanguagePatterns = PathPattern.create(patterns);
+ patternsByLanguage.put(language.key(), defaultLanguagePatterns);
+ LOG.debug("Declared extensions of language " + language + " were converted to " + getDetails(language.key()));
+ }
+ }
+
+ forcedLanguage = StringUtils.defaultIfBlank(settings.getString(CoreProperties.PROJECT_LANGUAGE_PROPERTY), null);
+ // First try with lang patterns
+ if (forcedLanguage != null) {
+ if (!patternsByLanguage.containsKey(forcedLanguage)) {
+ throw MessageException.of("No language is installed with key '" + forcedLanguage + "'. Please update property '" + CoreProperties.PROJECT_LANGUAGE_PROPERTY + "'");
+ }
+ languagesToConsider.add(forcedLanguage);
+ } else {
+ languagesToConsider.addAll(patternsByLanguage.keySet());
+ }
+ }
+
+ Map<String, PathPattern[]> patternsByLanguage() {
+ return patternsByLanguage;
+ }
+
+ @CheckForNull
+ String language(InputFile inputFile) {
+ String detectedLanguage = null;
+ for (String languageKey : languagesToConsider) {
+ if (isCandidateForLanguage(inputFile, languageKey)) {
+ if (detectedLanguage == null) {
+ detectedLanguage = languageKey;
+ } else {
+ // Language was already forced by another pattern
+ throw MessageException.of(MessageFormat.format("Language of file ''{0}'' can not be decided as the file matches patterns of both {1} and {2}",
+ inputFile.relativePath(), getDetails(detectedLanguage), getDetails(languageKey)));
+ }
+ }
+ }
+ if (detectedLanguage != null) {
+ LOG.debug(String.format("Language of file '%s' is detected to be '%s'", inputFile.relativePath(), detectedLanguage));
+ return detectedLanguage;
+ }
+
+ // Check if deprecated sonar.language is used and we are on a language without declared extensions.
+ // Languages without declared suffixes match everything.
+ if (forcedLanguage != null && patternsByLanguage.get(forcedLanguage).length == 0) {
+ return forcedLanguage;
+ }
+ return null;
+ }
+
+ private boolean isCandidateForLanguage(InputFile inputFile, String languageKey) {
+ PathPattern[] patterns = patternsByLanguage.get(languageKey);
+ if (patterns != null) {
+ for (PathPattern pathPattern : patterns) {
+ if (pathPattern.match(inputFile, false)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private String getFileLangPatternPropKey(String languageKey) {
+ return "sonar.lang.patterns." + languageKey;
+ }
+
+ private String getDetails(String detectedLanguage) {
+ return getFileLangPatternPropKey(detectedLanguage) + " : " + Joiner.on(",").join(patternsByLanguage.get(detectedLanguage));
+ }
+
+ static String sanitizeExtension(String suffix) {
+ return StringUtils.lowerCase(StringUtils.removeStart(suffix, "."));
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactory.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactory.java
new file mode 100644
index 00000000000..b25a0038c64
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactory.java
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.config.Settings;
+import org.sonar.batch.repository.language.LanguagesRepository;
+
+@BatchSide
+public class LanguageDetectionFactory {
+ private final Settings settings;
+ private final LanguagesRepository languages;
+
+ public LanguageDetectionFactory(Settings settings, LanguagesRepository languages) {
+ this.settings = settings;
+ this.languages = languages;
+ }
+
+ public LanguageDetection create() {
+ return new LanguageDetection(settings, languages);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializer.java
new file mode 100644
index 00000000000..faab666579a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializer.java
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.io.FileUtils;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.TempFolder;
+
+import javax.annotation.CheckForNull;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * @since 3.5
+ */
+@BatchSide
+public class ModuleFileSystemInitializer {
+
+ private File baseDir;
+ private File workingDir;
+ private File buildDir;
+ private List<File> sourceDirsOrFiles = Lists.newArrayList();
+ private List<File> testDirsOrFiles = Lists.newArrayList();
+ private List<File> binaryDirs = Lists.newArrayList();
+
+ public ModuleFileSystemInitializer(ProjectDefinition module, TempFolder tempUtils, PathResolver pathResolver) {
+ baseDir = module.getBaseDir();
+ buildDir = module.getBuildDir();
+ initWorkingDir(module, tempUtils);
+ initBinaryDirs(module, pathResolver);
+ initSources(module, pathResolver);
+ initTests(module, pathResolver);
+ }
+
+ private void initWorkingDir(ProjectDefinition module, TempFolder tempUtils) {
+ workingDir = module.getWorkDir();
+ if (workingDir == null) {
+ workingDir = tempUtils.newDir("work");
+ } else {
+ try {
+ FileUtils.forceMkdir(workingDir);
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to create working dir: " + workingDir.getAbsolutePath(), e);
+ }
+ }
+ }
+
+ private void initSources(ProjectDefinition module, PathResolver pathResolver) {
+ for (String sourcePath : module.sources()) {
+ File dirOrFile = pathResolver.relativeFile(module.getBaseDir(), sourcePath);
+ if (dirOrFile.exists()) {
+ sourceDirsOrFiles.add(dirOrFile);
+ }
+ }
+ }
+
+ private void initTests(ProjectDefinition module, PathResolver pathResolver) {
+ for (String testPath : module.tests()) {
+ File dirOrFile = pathResolver.relativeFile(module.getBaseDir(), testPath);
+ if (dirOrFile.exists()) {
+ testDirsOrFiles.add(dirOrFile);
+ }
+ }
+ }
+
+ private void initBinaryDirs(ProjectDefinition module, PathResolver pathResolver) {
+ for (String path : module.getBinaries()) {
+ File dir = pathResolver.relativeFile(module.getBaseDir(), path);
+ binaryDirs.add(dir);
+ }
+ }
+
+ File baseDir() {
+ return baseDir;
+ }
+
+ File workingDir() {
+ return workingDir;
+ }
+
+ @CheckForNull
+ File buildDir() {
+ return buildDir;
+ }
+
+ List<File> sources() {
+ return sourceDirsOrFiles;
+ }
+
+ List<File> tests() {
+ return testDirsOrFiles;
+ }
+
+ /**
+ * @deprecated since 4.5.1 use SonarQube Java specific API
+ */
+ @Deprecated
+ List<File> binaryDirs() {
+ return binaryDirs;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleInputFileCache.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleInputFileCache.java
new file mode 100644
index 00000000000..5326b3fda4b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/ModuleInputFileCache.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+
+@BatchSide
+public class ModuleInputFileCache extends DefaultFileSystem.Cache {
+
+ private final String moduleKey;
+ private final InputPathCache inputPathCache;
+
+ public ModuleInputFileCache(ProjectDefinition projectDef, InputPathCache projectCache) {
+ this.moduleKey = projectDef.getKeyWithBranch();
+ this.inputPathCache = projectCache;
+ }
+
+ @Override
+ public Iterable<InputFile> inputFiles() {
+ return inputPathCache.filesByModule(moduleKey);
+ }
+
+ @Override
+ public InputFile inputFile(String relativePath) {
+ return inputPathCache.getFile(moduleKey, relativePath);
+ }
+
+ @Override
+ public InputDir inputDir(String relativePath) {
+ return inputPathCache.getDir(moduleKey, relativePath);
+ }
+
+ @Override
+ protected void doAdd(InputFile inputFile) {
+ inputPathCache.put(moduleKey, inputFile);
+ }
+
+ @Override
+ protected void doAdd(InputDir inputDir) {
+ inputPathCache.put(moduleKey, inputDir);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java
new file mode 100644
index 00000000000..bd63a81dfe2
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.batch.repository.FileData;
+
+import org.sonar.batch.repository.ProjectRepositories;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.fs.InputFile;
+
+class StatusDetection {
+
+ private final ProjectRepositories projectSettings;
+
+ StatusDetection(ProjectRepositories projectSettings) {
+ this.projectSettings = projectSettings;
+ }
+
+ InputFile.Status status(String projectKey, String relativePath, String hash) {
+ FileData fileDataPerPath = projectSettings.fileData(projectKey, relativePath);
+ if (fileDataPerPath == null) {
+ return InputFile.Status.ADDED;
+ }
+ String previousHash = fileDataPerPath.hash();
+ if (StringUtils.equals(hash, previousHash)) {
+ return InputFile.Status.SAME;
+ }
+ if (StringUtils.isEmpty(previousHash)) {
+ return InputFile.Status.ADDED;
+ }
+ return InputFile.Status.CHANGED;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java
new file mode 100644
index 00000000000..b1a75c228c0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.batch.repository.ProjectRepositories;
+
+import org.sonar.api.batch.BatchSide;
+
+@BatchSide
+public class StatusDetectionFactory {
+
+ private final ProjectRepositories projectReferentials;
+
+ public StatusDetectionFactory(ProjectRepositories projectReferentials) {
+ this.projectReferentials = projectReferentials;
+ }
+
+ StatusDetection create() {
+ return new StatusDetection(projectReferentials);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/package-info.java
new file mode 100644
index 00000000000..d5f662e81ea
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/filesystem/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/**
+ * This package is a part of bootstrap process, so we should take care about backward compatibility.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.batch.scan.filesystem;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DefaultMetricFinder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DefaultMetricFinder.java
new file mode 100644
index 00000000000..f453f1bb5f4
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DefaultMetricFinder.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.measure;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.batch.measure.Metric;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.measures.Metric.ValueType;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DefaultMetricFinder implements MetricFinder {
+
+ private Map<String, Metric> metricsByKey = new LinkedHashMap<>();
+
+ public DefaultMetricFinder(GlobalRepositories globalReferentials) {
+ for (org.sonar.scanner.protocol.input.Metric metric : globalReferentials.metrics()) {
+ metricsByKey.put(metric.key(), new org.sonar.api.measures.Metric.Builder(metric.key(), metric.key(), ValueType.valueOf(metric.valueType())).create());
+ }
+ }
+
+ @Override
+ public Metric findByKey(String key) {
+ return metricsByKey.get(key);
+ }
+
+ @Override
+ public Collection<Metric> findAll(List<String> metricKeys) {
+ List<Metric> result = Lists.newLinkedList();
+ for (String metricKey : metricKeys) {
+ Metric metric = findByKey(metricKey);
+ if (metric != null) {
+ result.add(metric);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Collection<Metric> findAll() {
+ return metricsByKey.values();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DeprecatedMetricFinder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DeprecatedMetricFinder.java
new file mode 100644
index 00000000000..09e6063355c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/DeprecatedMetricFinder.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.measure;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.Metric.ValueType;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+import org.sonar.api.measures.MetricFinder;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public final class DeprecatedMetricFinder implements MetricFinder {
+
+ private Map<String, Metric> metricsByKey = Maps.newLinkedHashMap();
+ private Map<Integer, Metric> metricsById = Maps.newLinkedHashMap();
+
+ public DeprecatedMetricFinder(GlobalRepositories globalReferentials) {
+ for (org.sonar.scanner.protocol.input.Metric metric : globalReferentials.metrics()) {
+ Metric hibernateMetric = new org.sonar.api.measures.Metric.Builder(metric.key(), metric.name(), ValueType.valueOf(metric.valueType()))
+ .create()
+ .setDirection(metric.direction())
+ .setQualitative(metric.isQualitative())
+ .setUserManaged(metric.isUserManaged())
+ .setDescription(metric.description())
+ .setOptimizedBestValue(metric.isOptimizedBestValue())
+ .setBestValue(metric.bestValue())
+ .setWorstValue(metric.worstValue())
+ .setId(metric.id());
+ metricsByKey.put(metric.key(), hibernateMetric);
+ metricsById.put(metric.id(), new org.sonar.api.measures.Metric.Builder(metric.key(), metric.key(), ValueType.valueOf(metric.valueType())).create().setId(metric.id()));
+ }
+ }
+
+ @Override
+ public Metric findById(int metricId) {
+ return metricsById.get(metricId);
+ }
+
+ @Override
+ public Metric findByKey(String key) {
+ return metricsByKey.get(key);
+ }
+
+ @Override
+ public Collection<Metric> findAll(List<String> metricKeys) {
+ List<Metric> result = Lists.newLinkedList();
+ for (String metricKey : metricKeys) {
+ Metric metric = findByKey(metricKey);
+ if (metric != null) {
+ result.add(metric);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Collection<Metric> findAll() {
+ return metricsByKey.values();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureCache.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureCache.java
new file mode 100644
index 00000000000..963f7e14b91
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureCache.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.measure;
+
+import com.google.common.base.Preconditions;
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Resource;
+import org.sonar.batch.index.Cache;
+import org.sonar.batch.index.Cache.Entry;
+import org.sonar.batch.index.Caches;
+
+/**
+ * Cache of all measures. This cache is shared amongst all project modules.
+ */
+@BatchSide
+public class MeasureCache {
+
+ private final Cache<Measure> cache;
+
+ public MeasureCache(Caches caches, MetricFinder metricFinder) {
+ caches.registerValueCoder(Measure.class, new MeasureValueCoder(metricFinder));
+ cache = caches.createCache("measures");
+ }
+
+ public Iterable<Entry<Measure>> entries() {
+ return cache.entries();
+ }
+
+ public Iterable<Measure> all() {
+ return cache.values();
+ }
+
+ public Iterable<Measure> byResource(Resource r) {
+ return byComponentKey(r.getEffectiveKey());
+ }
+
+ public Iterable<Measure> byComponentKey(String effectiveKey) {
+ return cache.values(effectiveKey);
+ }
+
+ @CheckForNull
+ public Measure byMetric(Resource r, String metricKey) {
+ return byMetric(r.getEffectiveKey(), metricKey);
+ }
+
+ @CheckForNull
+ public Measure byMetric(String resourceKey, String metricKey) {
+ return cache.get(resourceKey, metricKey);
+ }
+
+ public MeasureCache put(Resource resource, Measure measure) {
+ Preconditions.checkNotNull(resource.getEffectiveKey());
+ Preconditions.checkNotNull(measure.getMetricKey());
+ cache.put(resource.getEffectiveKey(), measure.getMetricKey(), measure);
+ return this;
+ }
+
+ public boolean contains(Resource resource, Measure measure) {
+ Preconditions.checkNotNull(resource.getEffectiveKey());
+ Preconditions.checkNotNull(measure.getMetricKey());
+ return cache.containsKey(resource.getEffectiveKey(), measure.getMetricKey());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureValueCoder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureValueCoder.java
new file mode 100644
index 00000000000..019410fd06b
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/MeasureValueCoder.java
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.measure;
+
+import com.persistit.Value;
+import com.persistit.encoding.CoderContext;
+import com.persistit.encoding.ValueCoder;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.PersistenceMode;
+
+class MeasureValueCoder implements ValueCoder {
+
+ private final MetricFinder metricFinder;
+
+ public MeasureValueCoder(MetricFinder metricFinder) {
+ this.metricFinder = metricFinder;
+ }
+
+ @Override
+ public void put(Value value, Object object, CoderContext context) {
+ Measure<?> m = (Measure) object;
+ value.putUTF(m.getMetricKey());
+ value.put(m.getValue());
+ putUTFOrNull(value, m.getData());
+ putUTFOrNull(value, m.getDescription());
+ value.putString(m.getAlertStatus() != null ? m.getAlertStatus().name() : null);
+ putUTFOrNull(value, m.getAlertText());
+ value.putDate(m.getDate());
+ value.put(m.getVariation1());
+ value.put(m.getVariation2());
+ value.put(m.getVariation3());
+ value.put(m.getVariation4());
+ value.put(m.getVariation5());
+ putUTFOrNull(value, m.getUrl());
+ Integer personId = m.getPersonId();
+ value.put(personId != null ? personId.intValue() : null);
+ PersistenceMode persistenceMode = m.getPersistenceMode();
+ value.putString(persistenceMode != null ? persistenceMode.name() : null);
+ }
+
+ private static void putUTFOrNull(Value value, @Nullable String utfOrNull) {
+ if (utfOrNull != null) {
+ value.putUTF(utfOrNull);
+ } else {
+ value.putNull();
+ }
+ }
+
+ @Override
+ public Object get(Value value, Class clazz, CoderContext context) {
+ Measure<?> m = new Measure();
+ String metricKey = value.getString();
+ org.sonar.api.batch.measure.Metric metric = metricFinder.findByKey(metricKey);
+ if (metric == null) {
+ throw new IllegalStateException("Unknow metric with key " + metricKey);
+ }
+ m.setMetric((org.sonar.api.measures.Metric) metric);
+ m.setRawValue(value.isNull(true) ? null : value.getDouble());
+ m.setData(value.getString());
+ m.setDescription(value.getString());
+ m.setAlertStatus(value.isNull(true) ? null : Metric.Level.valueOf(value.getString()));
+ m.setAlertText(value.getString());
+ m.setDate(value.getDate());
+ m.setVariation1(value.isNull(true) ? null : value.getDouble());
+ m.setVariation2(value.isNull(true) ? null : value.getDouble());
+ m.setVariation3(value.isNull(true) ? null : value.getDouble());
+ m.setVariation4(value.isNull(true) ? null : value.getDouble());
+ m.setVariation5(value.isNull(true) ? null : value.getDouble());
+ m.setUrl(value.getString());
+ m.setPersonId(value.isNull(true) ? null : value.getInt());
+ m.setPersistenceMode(value.isNull(true) ? null : PersistenceMode.valueOf(value.getString()));
+ return m;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/package-info.java
new file mode 100644
index 00000000000..f35eb86c95c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/measure/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/**
+ * This package is a part of bootstrap process, so we should take care about backward compatibility.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.batch.scan.measure;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/package-info.java
new file mode 100644
index 00000000000..af539896111
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.scan;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ConsoleReport.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ConsoleReport.java
new file mode 100644
index 00000000000..7e37b789c71
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ConsoleReport.java
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.PropertyType;
+import org.sonar.api.config.Settings;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+
+@Properties({
+ @Property(key = ConsoleReport.CONSOLE_REPORT_ENABLED_KEY, defaultValue = "false", name = "Enable console report",
+ description = "Set this to true to generate a report in console output", type = PropertyType.BOOLEAN)})
+public class ConsoleReport implements Reporter {
+
+ @VisibleForTesting
+ public static final String HEADER = "------------- Issues Report -------------";
+
+ private static final Logger LOG = Loggers.get(ConsoleReport.class);
+
+ public static final String CONSOLE_REPORT_ENABLED_KEY = "sonar.issuesReport.console.enable";
+ private static final int LEFT_PAD = 10;
+
+ private Settings settings;
+ private IssueCache issueCache;
+ private InputPathCache inputPathCache;
+
+ @VisibleForTesting
+ public ConsoleReport(Settings settings, IssueCache issueCache, InputPathCache inputPathCache) {
+ this.settings = settings;
+ this.issueCache = issueCache;
+ this.inputPathCache = inputPathCache;
+ }
+
+ private static class Report {
+ boolean noFile = false;
+ int totalNewIssues = 0;
+ int newBlockerIssues = 0;
+ int newCriticalIssues = 0;
+ int newMajorIssues = 0;
+ int newMinorIssues = 0;
+ int newInfoIssues = 0;
+
+ public void process(TrackedIssue issue) {
+ if (issue.isNew()) {
+ totalNewIssues++;
+ switch (issue.severity()) {
+ case Severity.BLOCKER:
+ newBlockerIssues++;
+ break;
+ case Severity.CRITICAL:
+ newCriticalIssues++;
+ break;
+ case Severity.MAJOR:
+ newMajorIssues++;
+ break;
+ case Severity.MINOR:
+ newMinorIssues++;
+ break;
+ case Severity.INFO:
+ newInfoIssues++;
+ break;
+ default:
+ throw new IllegalStateException("Unknow severity: " + issue.severity());
+ }
+ }
+ }
+
+ public void setNoFile(boolean value) {
+ this.noFile = value;
+ }
+ }
+
+ @Override
+ public void execute() {
+ if (settings.getBoolean(CONSOLE_REPORT_ENABLED_KEY)) {
+ Report r = new Report();
+ r.setNoFile(!inputPathCache.allFiles().iterator().hasNext());
+ for (TrackedIssue issue : issueCache.all()) {
+ r.process(issue);
+ }
+ printReport(r);
+ }
+ }
+
+ public void printReport(Report r) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("\n\n" + HEADER + "\n\n");
+ if (r.noFile) {
+ sb.append(" No file analyzed\n");
+ } else {
+ printNewIssues(r, sb);
+ }
+ sb.append("\n-------------------------------------------\n\n");
+
+ LOG.info(sb.toString());
+ }
+
+ private static void printNewIssues(Report r, StringBuilder sb) {
+ int newIssues = r.totalNewIssues;
+ if (newIssues > 0) {
+ sb.append(StringUtils.leftPad("+" + newIssues, LEFT_PAD)).append(" issue" + (newIssues > 1 ? "s" : "")).append("\n\n");
+ printNewIssues(sb, r.newBlockerIssues, Severity.BLOCKER, "blocker");
+ printNewIssues(sb, r.newCriticalIssues, Severity.CRITICAL, "critical");
+ printNewIssues(sb, r.newMajorIssues, Severity.MAJOR, "major");
+ printNewIssues(sb, r.newMinorIssues, Severity.MINOR, "minor");
+ printNewIssues(sb, r.newInfoIssues, Severity.INFO, "info");
+ } else {
+ sb.append(" No new issue").append("\n");
+ }
+ }
+
+ private static void printNewIssues(StringBuilder sb, int issueCount, String severity, String severityLabel) {
+ if (issueCount > 0) {
+ sb.append(StringUtils.leftPad("+" + issueCount, LEFT_PAD)).append(" ").append(severityLabel).append("\n");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/HtmlReport.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/HtmlReport.java
new file mode 100644
index 00000000000..8111abd467e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/HtmlReport.java
@@ -0,0 +1,179 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import com.google.common.collect.Maps;
+import freemarker.template.Template;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URISyntaxException;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.PropertyType;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.config.Settings;
+
+@Properties({
+ @Property(key = HtmlReport.HTML_REPORT_ENABLED_KEY, defaultValue = "false", name = "Enable HTML report", description = "Set this to true to generate an HTML report",
+ type = PropertyType.BOOLEAN),
+ @Property(key = HtmlReport.HTML_REPORT_LOCATION_KEY, defaultValue = HtmlReport.HTML_REPORT_LOCATION_DEFAULT, name = "HTML Report location",
+ description = "Location of the generated report. Can be absolute or relative to working directory", project = false, global = false, type = PropertyType.STRING),
+ @Property(key = HtmlReport.HTML_REPORT_NAME_KEY, defaultValue = HtmlReport.HTML_REPORT_NAME_DEFAULT, name = "HTML Report name",
+ description = "Name of the generated report. Will be suffixed by .html or -light.html", project = false, global = false, type = PropertyType.STRING),
+ @Property(key = HtmlReport.HTML_REPORT_LIGHTMODE_ONLY, defaultValue = "false", name = "Html report in light mode only",
+ description = "Set this to true to only generate the new issues report (light report)", project = true, type = PropertyType.BOOLEAN)})
+public class HtmlReport implements Reporter {
+ private static final Logger LOG = LoggerFactory.getLogger(HtmlReport.class);
+
+ public static final String HTML_REPORT_ENABLED_KEY = "sonar.issuesReport.html.enable";
+ public static final String HTML_REPORT_LOCATION_KEY = "sonar.issuesReport.html.location";
+ public static final String HTML_REPORT_LOCATION_DEFAULT = "issues-report";
+ public static final String HTML_REPORT_NAME_KEY = "sonar.issuesReport.html.name";
+ public static final String HTML_REPORT_NAME_DEFAULT = "issues-report";
+ public static final String HTML_REPORT_LIGHTMODE_ONLY = "sonar.issuesReport.lightModeOnly";
+
+ private final Settings settings;
+ private final FileSystem fs;
+ private final IssuesReportBuilder builder;
+ private final SourceProvider sourceProvider;
+ private final RuleNameProvider ruleNameProvider;
+
+ public HtmlReport(Settings settings, FileSystem fs, IssuesReportBuilder builder, SourceProvider sourceProvider, RuleNameProvider ruleNameProvider) {
+ this.settings = settings;
+ this.fs = fs;
+ this.builder = builder;
+ this.sourceProvider = sourceProvider;
+ this.ruleNameProvider = ruleNameProvider;
+ }
+
+ @Override
+ public void execute() {
+ if (settings.getBoolean(HTML_REPORT_ENABLED_KEY)) {
+ IssuesReport report = builder.buildReport();
+ print(report);
+ }
+ }
+
+ public void print(IssuesReport report) {
+ File reportFileDir = getReportFileDir();
+ String reportName = settings.getString(HTML_REPORT_NAME_KEY);
+ if (!isLightModeOnly()) {
+ File reportFile = new File(reportFileDir, reportName + ".html");
+ LOG.debug("Generating HTML Report to: " + reportFile.getAbsolutePath());
+ writeToFile(report, reportFile, true);
+ LOG.info("HTML Issues Report generated: " + reportFile.getAbsolutePath());
+ }
+ File lightReportFile = new File(reportFileDir, reportName + "-light.html");
+ LOG.debug("Generating Light HTML Report to: " + lightReportFile.getAbsolutePath());
+ writeToFile(report, lightReportFile, false);
+ LOG.info("Light HTML Issues Report generated: " + lightReportFile.getAbsolutePath());
+ try {
+ copyDependencies(reportFileDir);
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to copy HTML report resources to: " + reportFileDir, e);
+ }
+ }
+
+ private File getReportFileDir() {
+ String reportFileDirStr = settings.getString(HTML_REPORT_LOCATION_KEY);
+ File reportFileDir = new File(reportFileDirStr);
+ if (!reportFileDir.isAbsolute()) {
+ reportFileDir = new File(fs.workDir(), reportFileDirStr);
+ }
+ if (StringUtils.endsWith(reportFileDirStr, ".html")) {
+ LOG.warn(HTML_REPORT_LOCATION_KEY + " should indicate a directory. Using parent folder.");
+ reportFileDir = reportFileDir.getParentFile();
+ }
+ try {
+ FileUtils.forceMkdir(reportFileDir);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create the directory " + reportFileDirStr, e);
+ }
+ return reportFileDir;
+ }
+
+ public void writeToFile(IssuesReport report, File toFile, boolean complete) {
+ try {
+ freemarker.log.Logger.selectLoggerLibrary(freemarker.log.Logger.LIBRARY_NONE);
+ freemarker.template.Configuration cfg = new freemarker.template.Configuration();
+ cfg.setClassForTemplateLoading(HtmlReport.class, "");
+
+ Map<String, Object> root = Maps.newHashMap();
+ root.put("report", report);
+ root.put("ruleNameProvider", ruleNameProvider);
+ root.put("sourceProvider", sourceProvider);
+ root.put("complete", complete);
+
+ Template template = cfg.getTemplate("issuesreport.ftl");
+
+ try (FileOutputStream fos = new FileOutputStream(toFile); Writer writer = new OutputStreamWriter(fos, fs.encoding())) {
+ template.process(root, writer);
+ writer.flush();
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to generate HTML Issues Report to: " + toFile, e);
+
+ }
+ }
+
+ void copyDependencies(File toDir) throws URISyntaxException, IOException {
+ File target = new File(toDir, "issuesreport_files");
+ FileUtils.forceMkdir(target);
+
+ // I don't know how to extract a directory from classpath, that's why an exhaustive list of files
+ // is provided here :
+ copyDependency(target, "sonar.eot");
+ copyDependency(target, "sonar.svg");
+ copyDependency(target, "sonar.ttf");
+ copyDependency(target, "sonar.woff");
+ copyDependency(target, "favicon.ico");
+ copyDependency(target, "PRJ.png");
+ copyDependency(target, "DIR.png");
+ copyDependency(target, "FIL.png");
+ copyDependency(target, "jquery.min.js");
+ copyDependency(target, "sep12.png");
+ copyDependency(target, "sonar.css");
+ copyDependency(target, "sonarqube-24x100.png");
+ }
+
+ private void copyDependency(File target, String filename) {
+ try (InputStream input = getClass().getResourceAsStream("/org/sonar/batch/scan/report/issuesreport_files/" + filename);
+ OutputStream output = new FileOutputStream(new File(target, filename))) {
+ IOUtils.copy(input, output);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to copy file " + filename + " to " + target, e);
+ }
+ }
+
+ public boolean isLightModeOnly() {
+ return settings.getBoolean(HTML_REPORT_LIGHTMODE_ONLY);
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssueVariation.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssueVariation.java
new file mode 100644
index 00000000000..23ca58e7d8c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssueVariation.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+public class IssueVariation {
+
+ private int countInCurrentAnalysis;
+ private int newIssuesCount;
+ private int resolvedIssuesCount;
+
+ public int getCountInCurrentAnalysis() {
+ return countInCurrentAnalysis;
+ }
+
+ public void incrementCountInCurrentAnalysis() {
+ this.countInCurrentAnalysis++;
+ }
+
+ public int getNewIssuesCount() {
+ return newIssuesCount;
+ }
+
+ public void incrementNewIssuesCount() {
+ this.newIssuesCount++;
+ }
+
+ public int getResolvedIssuesCount() {
+ return resolvedIssuesCount;
+ }
+
+ public void incrementResolvedIssuesCount() {
+ this.resolvedIssuesCount++;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).
+ append("countInCurrentAnalysis", countInCurrentAnalysis).
+ append("newIssuesCount", newIssuesCount).
+ append("resolvedIssuesCount", resolvedIssuesCount).
+ toString();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReport.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReport.java
new file mode 100644
index 00000000000..cede7a81d1c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReport.java
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.batch.index.BatchComponent;
+
+public class IssuesReport {
+
+ public static final int TOO_MANY_ISSUES_THRESHOLD = 1000;
+ private String title;
+ private Date date;
+ private boolean noFile;
+ private final ReportSummary summary = new ReportSummary();
+ private final Map<BatchComponent, ResourceReport> resourceReportsByResource = Maps.newLinkedHashMap();
+
+ public ReportSummary getSummary() {
+ return summary;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ public boolean isNoFile() {
+ return noFile;
+ }
+
+ public void setNoFile(boolean noFile) {
+ this.noFile = noFile;
+ }
+
+ public Map<BatchComponent, ResourceReport> getResourceReportsByResource() {
+ return resourceReportsByResource;
+ }
+
+ public List<ResourceReport> getResourceReports() {
+ return new ArrayList<>(resourceReportsByResource.values());
+ }
+
+ public List<BatchComponent> getResourcesWithReport() {
+ return new ArrayList<>(resourceReportsByResource.keySet());
+ }
+
+ public void addIssueOnResource(BatchComponent resource, TrackedIssue issue, Rule rule, RulePriority severity) {
+ addResource(resource);
+ getSummary().addIssue(issue, rule, severity);
+ resourceReportsByResource.get(resource).addIssue(issue, rule, RulePriority.valueOf(issue.severity()));
+ }
+
+ public void addResolvedIssueOnResource(BatchComponent resource, TrackedIssue issue, Rule rule, RulePriority severity) {
+ addResource(resource);
+ getSummary().addResolvedIssue(issue, rule, severity);
+ resourceReportsByResource.get(resource).addResolvedIssue(rule, RulePriority.valueOf(issue.severity()));
+ }
+
+ private void addResource(BatchComponent resource) {
+ if (!resourceReportsByResource.containsKey(resource)) {
+ resourceReportsByResource.put(resource, new ResourceReport(resource));
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReportBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReportBuilder.java
new file mode 100644
index 00000000000..079dabc2ceb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReportBuilder.java
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import javax.annotation.CheckForNull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.batch.rule.Rules;
+import org.sonar.api.resources.Project;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.batch.DefaultProjectTree;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+
+@BatchSide
+public class IssuesReportBuilder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(IssuesReportBuilder.class);
+
+ private final IssueCache issueCache;
+ private final Rules rules;
+ private final BatchComponentCache resourceCache;
+ private final DefaultProjectTree projectTree;
+ private final InputPathCache inputPathCache;
+
+ public IssuesReportBuilder(IssueCache issueCache, Rules rules, BatchComponentCache resourceCache, DefaultProjectTree projectTree, InputPathCache inputPathCache) {
+ this.issueCache = issueCache;
+ this.rules = rules;
+ this.resourceCache = resourceCache;
+ this.projectTree = projectTree;
+ this.inputPathCache = inputPathCache;
+ }
+
+ public IssuesReport buildReport() {
+ Project project = projectTree.getRootProject();
+ IssuesReport issuesReport = new IssuesReport();
+ issuesReport.setNoFile(!inputPathCache.allFiles().iterator().hasNext());
+ issuesReport.setTitle(project.getName());
+ issuesReport.setDate(project.getAnalysisDate());
+
+ processIssues(issuesReport, issueCache.all());
+
+ return issuesReport;
+ }
+
+ private void processIssues(IssuesReport issuesReport, Iterable<TrackedIssue> issues) {
+ for (TrackedIssue issue : issues) {
+ Rule rule = findRule(issue);
+ RulePriority severity = RulePriority.valueOf(issue.severity());
+ BatchComponent resource = resourceCache.get(issue.componentKey());
+ if (!validate(issue, rule, resource)) {
+ continue;
+ }
+ if (issue.resolution() != null) {
+ issuesReport.addResolvedIssueOnResource(resource, issue, rule, severity);
+ } else {
+ issuesReport.addIssueOnResource(resource, issue, rule, severity);
+ }
+ }
+ }
+
+ private static boolean validate(TrackedIssue issue, Rule rule, BatchComponent resource) {
+ if (rule == null) {
+ LOG.warn("Unknow rule for issue {}", issue);
+ return false;
+ }
+ if (resource == null) {
+ LOG.debug("Unknow resource with key {}", issue.componentKey());
+ return false;
+ }
+ return true;
+ }
+
+ @CheckForNull
+ private Rule findRule(TrackedIssue issue) {
+ return rules.find(issue.getRuleKey());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReports.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReports.java
new file mode 100644
index 00000000000..ac4a5abb375
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/IssuesReports.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.api.batch.BatchSide;
+
+@BatchSide
+public class IssuesReports {
+
+ private final Reporter[] reporters;
+
+ public IssuesReports(Reporter... reporters) {
+ this.reporters = reporters;
+ }
+
+ public void execute() {
+ for (Reporter reporter : reporters) {
+ reporter.execute();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/JSONReport.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/JSONReport.java
new file mode 100644
index 00000000000..99954736613
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/JSONReport.java
@@ -0,0 +1,247 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.BufferedWriter;
+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 java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.PropertyType;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputDir;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.batch.rule.Rules;
+import org.sonar.api.config.Settings;
+import org.sonar.api.platform.Server;
+import org.sonar.api.resources.Project;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+import org.sonar.scanner.protocol.input.ScannerInput;
+import org.sonar.scanner.protocol.input.ScannerInput.User;
+
+@Properties({
+ @Property(
+ key = JSONReport.SONAR_REPORT_EXPORT_PATH,
+ name = "Report Results Export File",
+ type = PropertyType.STRING,
+ global = false, project = false)})
+public class JSONReport implements Reporter {
+
+ static final String SONAR_REPORT_EXPORT_PATH = "sonar.report.export.path";
+ private static final Logger LOG = LoggerFactory.getLogger(JSONReport.class);
+ private final Settings settings;
+ private final FileSystem fileSystem;
+ private final Server server;
+ private final Rules rules;
+ private final IssueCache issueCache;
+ private final InputPathCache fileCache;
+ private final Project rootModule;
+ private final UserRepositoryLoader userRepository;
+
+ public JSONReport(Settings settings, FileSystem fileSystem, Server server, Rules rules, IssueCache issueCache,
+ Project rootModule, InputPathCache fileCache, UserRepositoryLoader userRepository) {
+ this.settings = settings;
+ this.fileSystem = fileSystem;
+ this.server = server;
+ this.rules = rules;
+ this.issueCache = issueCache;
+ this.rootModule = rootModule;
+ this.fileCache = fileCache;
+ this.userRepository = userRepository;
+ }
+
+ @Override
+ public void execute() {
+ String exportPath = settings.getString(SONAR_REPORT_EXPORT_PATH);
+ if (exportPath != null) {
+ exportResults(exportPath);
+ }
+ }
+
+ private void exportResults(String exportPath) {
+ File exportFile = new File(fileSystem.workDir(), exportPath);
+
+ LOG.info("Export issues to {}", exportFile.getAbsolutePath());
+ try (Writer output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(exportFile), StandardCharsets.UTF_8))) {
+ writeJson(output);
+
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to write report results in file " + exportFile.getAbsolutePath(), e);
+ }
+ }
+
+ @VisibleForTesting
+ void writeJson(Writer writer) {
+ try {
+ JsonWriter json = JsonWriter.of(writer);
+ json.beginObject();
+ json.prop("version", server.getVersion());
+
+ Set<RuleKey> ruleKeys = new LinkedHashSet<>();
+ Set<String> userLogins = new LinkedHashSet<>();
+ writeJsonIssues(json, ruleKeys, userLogins);
+ writeJsonComponents(json);
+ writeJsonRules(json, ruleKeys);
+ writeUsers(json, userLogins);
+ json.endObject().close();
+
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to write JSON report", e);
+ }
+ }
+
+ private void writeJsonIssues(JsonWriter json, Set<RuleKey> ruleKeys, Set<String> logins) throws IOException {
+ json.name("issues").beginArray();
+ for (TrackedIssue issue : getIssues()) {
+ if (issue.resolution() == null) {
+ json
+ .beginObject()
+ .prop("key", issue.key())
+ .prop("component", issue.componentKey())
+ .prop("line", issue.startLine())
+ .prop("startLine", issue.startLine())
+ .prop("startOffset", issue.startLineOffset())
+ .prop("endLine", issue.endLine())
+ .prop("endOffset", issue.endLineOffset())
+ .prop("message", issue.getMessage())
+ .prop("severity", issue.severity())
+ .prop("rule", issue.getRuleKey().toString())
+ .prop("status", issue.status())
+ .prop("resolution", issue.resolution())
+ .prop("isNew", issue.isNew())
+ .prop("assignee", issue.assignee())
+ .prop("effortToFix", issue.gap())
+ .propDateTime("creationDate", issue.creationDate());
+ if (!StringUtils.isEmpty(issue.reporter())) {
+ logins.add(issue.reporter());
+ }
+ if (!StringUtils.isEmpty(issue.assignee())) {
+ logins.add(issue.assignee());
+ }
+ json.endObject();
+ ruleKeys.add(issue.getRuleKey());
+ }
+ }
+ json.endArray();
+ }
+
+ private void writeJsonComponents(JsonWriter json) throws IOException {
+ json.name("components").beginArray();
+ // Dump modules
+ writeJsonModuleComponents(json, rootModule);
+ for (InputFile inputFile : fileCache.allFiles()) {
+ String key = ((DefaultInputFile) inputFile).key();
+ json
+ .beginObject()
+ .prop("key", key)
+ .prop("path", inputFile.relativePath())
+ .prop("moduleKey", StringUtils.substringBeforeLast(key, ":"))
+ .prop("status", inputFile.status().name())
+ .endObject();
+ }
+ for (InputDir inputDir : fileCache.allDirs()) {
+ String key = ((DefaultInputDir) inputDir).key();
+ json
+ .beginObject()
+ .prop("key", key)
+ .prop("path", inputDir.relativePath())
+ .prop("moduleKey", StringUtils.substringBeforeLast(key, ":"))
+ .endObject();
+
+ }
+ json.endArray();
+ }
+
+ private static void writeJsonModuleComponents(JsonWriter json, Project module) {
+ json
+ .beginObject()
+ .prop("key", module.getEffectiveKey())
+ .prop("path", module.getPath())
+ .endObject();
+ for (Project subModule : module.getModules()) {
+ writeJsonModuleComponents(json, subModule);
+ }
+ }
+
+ private void writeJsonRules(JsonWriter json, Set<RuleKey> ruleKeys) throws IOException {
+ json.name("rules").beginArray();
+ for (RuleKey ruleKey : ruleKeys) {
+ json
+ .beginObject()
+ .prop("key", ruleKey.toString())
+ .prop("rule", ruleKey.rule())
+ .prop("repository", ruleKey.repository())
+ .prop("name", getRuleName(ruleKey))
+ .endObject();
+ }
+ json.endArray();
+ }
+
+ private void writeUsers(JsonWriter json, Collection<String> userLogins) throws IOException {
+ List<ScannerInput.User> users = new LinkedList<>();
+ for (String userLogin : userLogins) {
+ User user = userRepository.load(userLogin);
+ if (user != null) {
+ users.add(user);
+ }
+ }
+
+ json.name("users").beginArray();
+ for (ScannerInput.User user : users) {
+ json
+ .beginObject()
+ .prop("login", user.getLogin())
+ .prop("name", user.getName())
+ .endObject();
+ }
+ json.endArray();
+ }
+
+ private String getRuleName(RuleKey ruleKey) {
+ Rule rule = rules.find(ruleKey);
+ return rule != null ? rule.name() : null;
+ }
+
+ @VisibleForTesting
+ Iterable<TrackedIssue> getIssues() {
+ return issueCache.all();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportRuleKey.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportRuleKey.java
new file mode 100644
index 00000000000..1061d3c3238
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportRuleKey.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.api.batch.rule.Rule;
+
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.sonar.api.rules.RulePriority;
+
+/**
+ * A same rule can be present with different severity if severity was manually changed so we need this special key that
+ * include severity.
+ *
+ */
+public class ReportRuleKey implements Comparable<ReportRuleKey> {
+ private final Rule rule;
+ private final RulePriority severity;
+
+ public ReportRuleKey(Rule rule, RulePriority severity) {
+ this.rule = rule;
+ this.severity = severity;
+ }
+
+ public Rule getRule() {
+ return rule;
+ }
+
+ public RulePriority getSeverity() {
+ return severity;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ReportRuleKey that = (ReportRuleKey) o;
+ return ObjectUtils.equals(rule, that.rule) && ObjectUtils.equals(severity, that.severity);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = rule.hashCode();
+ result = 31 * result + severity.hashCode();
+ return result;
+ }
+
+ @Override
+ public int compareTo(ReportRuleKey o) {
+ if (severity == o.getSeverity()) {
+ return getRule().key().toString().compareTo(o.getRule().key().toString());
+ }
+ return o.getSeverity().compareTo(severity);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).
+ append("rule", rule).
+ append("severity", severity).
+ toString();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportSummary.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportSummary.java
new file mode 100644
index 00000000000..435aee37450
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ReportSummary.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import com.google.common.collect.Maps;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+public class ReportSummary {
+
+ private final IssueVariation total = new IssueVariation();
+
+ private final Map<ReportRuleKey, RuleReport> ruleReportByRuleKey = Maps.newLinkedHashMap();
+ private final Map<String, IssueVariation> totalByRuleKey = Maps.newLinkedHashMap();
+ private final Map<String, IssueVariation> totalBySeverity = Maps.newLinkedHashMap();
+
+ public IssueVariation getTotal() {
+ return total;
+ }
+
+ public void addIssue(TrackedIssue issue, Rule rule, RulePriority severity) {
+ ReportRuleKey reportRuleKey = new ReportRuleKey(rule, severity);
+ initMaps(reportRuleKey);
+ ruleReportByRuleKey.get(reportRuleKey).getTotal().incrementCountInCurrentAnalysis();
+ total.incrementCountInCurrentAnalysis();
+ totalByRuleKey.get(rule.key().toString()).incrementCountInCurrentAnalysis();
+ totalBySeverity.get(severity.toString()).incrementCountInCurrentAnalysis();
+ if (issue.isNew()) {
+ total.incrementNewIssuesCount();
+ ruleReportByRuleKey.get(reportRuleKey).getTotal().incrementNewIssuesCount();
+ totalByRuleKey.get(rule.key().toString()).incrementNewIssuesCount();
+ totalBySeverity.get(severity.toString()).incrementNewIssuesCount();
+ }
+ }
+
+ public Map<String, IssueVariation> getTotalBySeverity() {
+ return totalBySeverity;
+ }
+
+ public Map<String, IssueVariation> getTotalByRuleKey() {
+ return totalByRuleKey;
+ }
+
+ public void addResolvedIssue(TrackedIssue issue, Rule rule, RulePriority severity) {
+ ReportRuleKey reportRuleKey = new ReportRuleKey(rule, severity);
+ initMaps(reportRuleKey);
+ total.incrementResolvedIssuesCount();
+ ruleReportByRuleKey.get(reportRuleKey).getTotal().incrementResolvedIssuesCount();
+ totalByRuleKey.get(rule.key().toString()).incrementResolvedIssuesCount();
+ totalBySeverity.get(severity.toString()).incrementResolvedIssuesCount();
+ }
+
+ private void initMaps(ReportRuleKey reportRuleKey) {
+ if (!ruleReportByRuleKey.containsKey(reportRuleKey)) {
+ ruleReportByRuleKey.put(reportRuleKey, new RuleReport(reportRuleKey));
+ }
+ if (!totalByRuleKey.containsKey(reportRuleKey.getRule().key().toString())) {
+ totalByRuleKey.put(reportRuleKey.getRule().key().toString(), new IssueVariation());
+ }
+ if (!totalBySeverity.containsKey(reportRuleKey.getSeverity().toString())) {
+ totalBySeverity.put(reportRuleKey.getSeverity().toString(), new IssueVariation());
+ }
+ }
+
+ public List<RuleReport> getRuleReports() {
+ List<RuleReport> result = new ArrayList<>(ruleReportByRuleKey.values());
+ Collections.sort(result, new RuleReportComparator());
+ return result;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/Reporter.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/Reporter.java
new file mode 100644
index 00000000000..9cb91dc2fad
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/Reporter.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.api.batch.BatchSide;
+
+@BatchSide
+public interface Reporter {
+
+ void execute();
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ResourceReport.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ResourceReport.java
new file mode 100644
index 00000000000..6267335baf0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/ResourceReport.java
@@ -0,0 +1,155 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import org.sonar.api.batch.rule.Rule;
+import com.google.common.collect.Maps;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.batch.index.BatchComponent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class ResourceReport {
+ private final BatchComponent resource;
+ private final IssueVariation total = new IssueVariation();
+ private final Map<ReportRuleKey, RuleReport> ruleReportByRuleKey = Maps.newHashMap();
+
+ private List<TrackedIssue> issues = new ArrayList<>();
+ private Map<Integer, List<TrackedIssue>> issuesPerLine = Maps.newHashMap();
+ private Map<Integer, List<TrackedIssue>> newIssuesPerLine = Maps.newHashMap();
+ private Map<Rule, AtomicInteger> issuesByRule = Maps.newHashMap();
+ private Map<RulePriority, AtomicInteger> issuesBySeverity = Maps.newHashMap();
+
+ public ResourceReport(BatchComponent resource) {
+ this.resource = resource;
+ }
+
+ public BatchComponent getResourceNode() {
+ return resource;
+ }
+
+ public String getName() {
+ return resource.resource().getName();
+ }
+
+ public String getType() {
+ return resource.resource().getScope();
+ }
+
+ public IssueVariation getTotal() {
+ return total;
+ }
+
+ public List<TrackedIssue> getIssues() {
+ return issues;
+ }
+
+ public Map<Integer, List<TrackedIssue>> getIssuesPerLine() {
+ return issuesPerLine;
+ }
+
+ public List<TrackedIssue> getIssuesAtLine(int lineId, boolean all) {
+ if (all) {
+ if (issuesPerLine.containsKey(lineId)) {
+ return issuesPerLine.get(lineId);
+ }
+ } else if (newIssuesPerLine.containsKey(lineId)) {
+ return newIssuesPerLine.get(lineId);
+ }
+ return Collections.emptyList();
+ }
+
+ public void addIssue(TrackedIssue issue, Rule rule, RulePriority severity) {
+ ReportRuleKey reportRuleKey = new ReportRuleKey(rule, severity);
+ initMaps(reportRuleKey);
+ issues.add(issue);
+ Integer line = issue.startLine();
+ line = line != null ? line : 0;
+ if (!issuesPerLine.containsKey(line)) {
+ issuesPerLine.put(line, new ArrayList<TrackedIssue>());
+ }
+ issuesPerLine.get(line).add(issue);
+ if (!issuesByRule.containsKey(rule)) {
+ issuesByRule.put(rule, new AtomicInteger());
+ }
+ issuesByRule.get(rule).incrementAndGet();
+ if (!issuesBySeverity.containsKey(severity)) {
+ issuesBySeverity.put(severity, new AtomicInteger());
+ }
+ issuesBySeverity.get(severity).incrementAndGet();
+ ruleReportByRuleKey.get(reportRuleKey).getTotal().incrementCountInCurrentAnalysis();
+ total.incrementCountInCurrentAnalysis();
+ if (issue.isNew()) {
+ if (!newIssuesPerLine.containsKey(line)) {
+ newIssuesPerLine.put(line, new ArrayList<TrackedIssue>());
+ }
+ newIssuesPerLine.get(line).add(issue);
+ total.incrementNewIssuesCount();
+ ruleReportByRuleKey.get(reportRuleKey).getTotal().incrementNewIssuesCount();
+ }
+ }
+
+ public void addResolvedIssue(Rule rule, RulePriority severity) {
+ ReportRuleKey reportRuleKey = new ReportRuleKey(rule, severity);
+ initMaps(reportRuleKey);
+ total.incrementResolvedIssuesCount();
+ ruleReportByRuleKey.get(reportRuleKey).getTotal().incrementResolvedIssuesCount();
+ }
+
+ private void initMaps(ReportRuleKey reportRuleKey) {
+ if (!ruleReportByRuleKey.containsKey(reportRuleKey)) {
+ ruleReportByRuleKey.put(reportRuleKey, new RuleReport(reportRuleKey));
+ }
+ }
+
+ public boolean isDisplayableLine(Integer lineNumber, boolean all) {
+ if (lineNumber == null || lineNumber < 1) {
+ return false;
+ }
+ for (int i = lineNumber - 2; i <= lineNumber + 2; i++) {
+ if (hasIssues(i, all)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasIssues(Integer lineId, boolean all) {
+ if (all) {
+ List<TrackedIssue> issuesAtLine = issuesPerLine.get(lineId);
+ return issuesAtLine != null && !issuesAtLine.isEmpty();
+ }
+ List<TrackedIssue> newIssuesAtLine = newIssuesPerLine.get(lineId);
+ return newIssuesAtLine != null && !newIssuesAtLine.isEmpty();
+ }
+
+ public List<RuleReport> getRuleReports() {
+ List<RuleReport> result = new ArrayList<>(ruleReportByRuleKey.values());
+ Collections.sort(result, new RuleReportComparator());
+ return result;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleNameProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleNameProvider.java
new file mode 100644
index 00000000000..90981fea342
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleNameProvider.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.api.batch.rule.Rule;
+
+import org.sonar.api.batch.rule.Rules;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.rule.RuleKey;
+
+import javax.annotation.CheckForNull;
+
+@BatchSide
+public class RuleNameProvider {
+ private Rules rules;
+
+ public RuleNameProvider(Rules rules) {
+ this.rules = rules;
+ }
+
+ @CheckForNull
+ private String nameFromDB(RuleKey ruleKey) {
+ Rule r = rules.find(ruleKey);
+ return r != null ? r.name() : null;
+ }
+
+ public String nameForHTML(RuleKey ruleKey) {
+ String name = nameFromDB(ruleKey);
+ return StringEscapeUtils.escapeHtml(name != null ? name : ruleKey.toString());
+ }
+
+ public String nameForJS(String ruleKey) {
+ String name = nameFromDB(RuleKey.parse(ruleKey));
+ return StringEscapeUtils.escapeJavaScript(name != null ? name : ruleKey);
+ }
+
+ public String nameForHTML(Rule rule) {
+ return StringEscapeUtils.escapeHtml(rule.name());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReport.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReport.java
new file mode 100644
index 00000000000..4026ed387ab
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReport.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import org.sonar.api.batch.rule.Rule;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+public final class RuleReport {
+ private final ReportRuleKey reportRuleKey;
+ private final IssueVariation total = new IssueVariation();
+
+ public RuleReport(ReportRuleKey reportRuleKey) {
+ this.reportRuleKey = reportRuleKey;
+ }
+
+ public IssueVariation getTotal() {
+ return total;
+ }
+
+ public ReportRuleKey getReportRuleKey() {
+ return reportRuleKey;
+ }
+
+ public String getSeverity() {
+ return reportRuleKey.getSeverity().toString();
+ }
+
+ public Rule getRule() {
+ return reportRuleKey.getRule();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).
+ append("reportRuleKey", reportRuleKey).
+ append("total", total).
+ toString();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReportComparator.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReportComparator.java
new file mode 100644
index 00000000000..b779537e8fd
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/RuleReportComparator.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+public class RuleReportComparator implements Comparator<RuleReport>, Serializable {
+ @Override
+ public int compare(RuleReport o1, RuleReport o2) {
+ if (bothHaveNoNewIssue(o1, o2)) {
+ return compareByRuleSeverityAndName(o1, o2);
+ } else if (bothHaveNewIssues(o1, o2)) {
+ if (sameSeverity(o1, o2) && !sameNewIssueCount(o1, o2)) {
+ return compareNewIssueCount(o1, o2);
+ } else {
+ return compareByRuleSeverityAndName(o1, o2);
+ }
+ } else {
+ return compareNewIssueCount(o1, o2);
+ }
+ }
+
+ private static int compareByRuleSeverityAndName(RuleReport o1, RuleReport o2) {
+ return o1.getReportRuleKey().compareTo(o2.getReportRuleKey());
+ }
+
+ private static boolean sameNewIssueCount(RuleReport o1, RuleReport o2) {
+ return o2.getTotal().getNewIssuesCount() == o1.getTotal().getNewIssuesCount();
+ }
+
+ private static boolean sameSeverity(RuleReport o1, RuleReport o2) {
+ return o1.getSeverity().equals(o2.getSeverity());
+ }
+
+ private static int compareNewIssueCount(RuleReport o1, RuleReport o2) {
+ return o2.getTotal().getNewIssuesCount() - o1.getTotal().getNewIssuesCount();
+ }
+
+ private static boolean bothHaveNewIssues(RuleReport o1, RuleReport o2) {
+ return o1.getTotal().getNewIssuesCount() > 0 && o2.getTotal().getNewIssuesCount() > 0;
+ }
+
+ private static boolean bothHaveNoNewIssue(RuleReport o1, RuleReport o2) {
+ return o1.getTotal().getNewIssuesCount() == 0 && o2.getTotal().getNewIssuesCount() == 0;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/SourceProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/SourceProvider.java
new file mode 100644
index 00000000000..8017dbd753e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/SourceProvider.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.batch.index.BatchComponent;
+
+@BatchSide
+public class SourceProvider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SourceProvider.class);
+ private final FileSystem fs;
+
+ public SourceProvider(FileSystem fs) {
+ this.fs = fs;
+ }
+
+ public List<String> getEscapedSource(BatchComponent component) {
+ if (!component.isFile()) {
+ // Folder
+ return Collections.emptyList();
+ }
+ try {
+ InputFile inputFile = (InputFile) component.inputComponent();
+ List<String> lines = FileUtils.readLines(inputFile.file(), fs.encoding());
+ List<String> escapedLines = new ArrayList<>(lines.size());
+ for (String line : lines) {
+ escapedLines.add(StringEscapeUtils.escapeHtml(line));
+ }
+ return escapedLines;
+ } catch (IOException e) {
+ LOG.warn("Unable to read source code of resource {}", component, e);
+ return Collections.emptyList();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/package-info.java
new file mode 100644
index 00000000000..d6dacad1b54
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scan/report/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.scan.report;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameInput.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameInput.java
new file mode 100644
index 00000000000..a85c6536c92
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameInput.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scm;
+
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.scm.BlameCommand.BlameInput;
+
+class DefaultBlameInput implements BlameInput {
+
+ private FileSystem fs;
+ private Iterable<InputFile> filesToBlame;
+
+ DefaultBlameInput(FileSystem fs, Iterable<InputFile> filesToBlame) {
+ this.fs = fs;
+ this.filesToBlame = filesToBlame;
+ }
+
+ @Override
+ public FileSystem fileSystem() {
+ return fs;
+ }
+
+ @Override
+ public Iterable<InputFile> filesToBlame() {
+ return filesToBlame;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameOutput.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameOutput.java
new file mode 100644
index 00000000000..69d7d5f2481
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/DefaultBlameOutput.java
@@ -0,0 +1,147 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scm;
+
+import com.google.common.base.Preconditions;
+import java.text.Normalizer;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.scm.BlameCommand.BlameOutput;
+import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.util.ProgressReport;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.protocol.output.ScannerReport.Changesets.Builder;
+
+class DefaultBlameOutput implements BlameOutput {
+
+ private static final Logger LOG = Loggers.get(DefaultBlameOutput.class);
+
+ private static final Pattern NON_ASCII_CHARS = Pattern.compile("[^\\x00-\\x7F]");
+ private static final Pattern ACCENT_CODES = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
+
+ private final ScannerReportWriter writer;
+ private final BatchComponentCache componentCache;
+ private final Set<InputFile> allFilesToBlame = new HashSet<>();
+ private ProgressReport progressReport;
+ private int count;
+ private int total;
+
+ DefaultBlameOutput(ScannerReportWriter writer, BatchComponentCache componentCache, List<InputFile> filesToBlame) {
+ this.writer = writer;
+ this.componentCache = componentCache;
+ this.allFilesToBlame.addAll(filesToBlame);
+ count = 0;
+ total = filesToBlame.size();
+ progressReport = new ProgressReport("Report about progress of SCM blame", TimeUnit.SECONDS.toMillis(10));
+ progressReport.start(total + " files to be analyzed");
+ }
+
+ @Override
+ public synchronized void blameResult(InputFile file, List<BlameLine> lines) {
+ Preconditions.checkNotNull(file);
+ Preconditions.checkNotNull(lines);
+ Preconditions.checkArgument(allFilesToBlame.contains(file), "It was not expected to blame file %s", file.relativePath());
+
+ if (lines.size() != file.lines()) {
+ LOG.debug("Ignoring blame result since provider returned {} blame lines but file {} has {} lines", lines.size(), file.relativePath(), file.lines());
+ return;
+ }
+
+ BatchComponent batchComponent = componentCache.get(file);
+ Builder scmBuilder = ScannerReport.Changesets.newBuilder();
+ scmBuilder.setComponentRef(batchComponent.batchId());
+ Map<String, Integer> changesetsIdByRevision = new HashMap<>();
+
+ int lineId = 1;
+ for (BlameLine line : lines) {
+ validateLine(line, lineId, file);
+ Integer changesetId = changesetsIdByRevision.get(line.revision());
+ if (changesetId == null) {
+ addChangeset(scmBuilder, line);
+ changesetId = scmBuilder.getChangesetCount() - 1;
+ changesetsIdByRevision.put(line.revision(), changesetId);
+ }
+ scmBuilder.addChangesetIndexByLine(changesetId);
+ lineId++;
+ }
+ writer.writeComponentChangesets(scmBuilder.build());
+ allFilesToBlame.remove(file);
+ count++;
+ progressReport.message(count + "/" + total + " files analyzed");
+ }
+
+ private static void validateLine(BlameLine line, int lineId, InputFile file) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(line.revision()), "Blame revision is blank for file %s at line %s", file.relativePath(), lineId);
+ Preconditions.checkArgument(line.date() != null, "Blame date is null for file %s at line %s", file.relativePath(), lineId);
+ }
+
+ private static void addChangeset(Builder scmBuilder, BlameLine line) {
+ ScannerReport.Changesets.Changeset.Builder changesetBuilder = ScannerReport.Changesets.Changeset.newBuilder();
+ changesetBuilder.setRevision(line.revision());
+ changesetBuilder.setDate(line.date().getTime());
+ if (StringUtils.isNotBlank(line.author())) {
+ changesetBuilder.setAuthor(normalizeString(line.author()));
+ }
+
+ scmBuilder.addChangeset(changesetBuilder.build());
+ }
+
+ private static String normalizeString(@Nullable String inputString) {
+ if (inputString == null) {
+ return "";
+ }
+ String lowerCasedString = inputString.toLowerCase();
+ String stringWithoutAccents = removeAccents(lowerCasedString);
+ return removeNonAsciiCharacters(stringWithoutAccents);
+ }
+
+ private static String removeAccents(String inputString) {
+ String unicodeDecomposedString = Normalizer.normalize(inputString, Normalizer.Form.NFD);
+ return ACCENT_CODES.matcher(unicodeDecomposedString).replaceAll("");
+ }
+
+ private static String removeNonAsciiCharacters(String inputString) {
+ return NON_ASCII_CHARS.matcher(inputString).replaceAll("_");
+ }
+
+ public void finish() {
+ progressReport.stop(count + "/" + total + " files analyzed");
+ if (!allFilesToBlame.isEmpty()) {
+ LOG.warn("Missing blame information for the following files:");
+ for (InputFile f : allFilesToBlame) {
+ LOG.warn(" * " + f.absolutePath());
+ }
+ LOG.warn("This may lead to missing/broken features in SonarQube");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmConfiguration.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmConfiguration.java
new file mode 100644
index 00000000000..f6a88bf4465
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmConfiguration.java
@@ -0,0 +1,155 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scm;
+
+import com.google.common.base.Joiner;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.commons.lang.StringUtils;
+import org.picocontainer.Startable;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.PropertyType;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.batch.scm.ScmProvider;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+
+@Properties({
+ @Property(
+ key = ScmConfiguration.FORCE_RELOAD_KEY,
+ defaultValue = "false",
+ name = "Force reloading of SCM information for all files",
+ description = "By default only files modified since previous analysis are inspected. Set this parameter to true to force the reloading.",
+ category = CoreProperties.CATEGORY_SCM,
+ project = false,
+ module = false,
+ global = false,
+ type = PropertyType.BOOLEAN)
+})
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+@BatchSide
+public final class ScmConfiguration implements Startable {
+ private static final Logger LOG = Loggers.get(ScmConfiguration.class);
+
+ public static final String FORCE_RELOAD_KEY = "sonar.scm.forceReloadAll";
+
+ private final ImmutableProjectReactor projectReactor;
+ private final Settings settings;
+ private final Map<String, ScmProvider> providerPerKey = new LinkedHashMap<>();
+ private final AnalysisMode analysisMode;
+
+ private ScmProvider provider;
+
+ public ScmConfiguration(ImmutableProjectReactor projectReactor, AnalysisMode analysisMode, Settings settings, ScmProvider... providers) {
+ this.projectReactor = projectReactor;
+ this.analysisMode = analysisMode;
+ this.settings = settings;
+ for (ScmProvider scmProvider : providers) {
+ providerPerKey.put(scmProvider.key(), scmProvider);
+ }
+ }
+
+ public ScmConfiguration(ImmutableProjectReactor projectReactor, AnalysisMode analysisMode, Settings settings) {
+ this(projectReactor, analysisMode, settings, new ScmProvider[0]);
+ }
+
+ @Override
+ public void start() {
+ if (analysisMode.isIssues()) {
+ return;
+ }
+ if (isDisabled()) {
+ LOG.debug("SCM Step is disabled by configuration");
+ return;
+ }
+ if (settings.hasKey(CoreProperties.SCM_PROVIDER_KEY)) {
+ String forcedProviderKey = settings.getString(CoreProperties.SCM_PROVIDER_KEY);
+ setProviderIfSupported(forcedProviderKey);
+ } else {
+ autodetection();
+ if (this.provider == null) {
+ considerOldScmUrl();
+ }
+ if (this.provider == null) {
+ LOG.warn("SCM provider autodetection failed. No SCM provider claims to support this project. Please use " + CoreProperties.SCM_PROVIDER_KEY
+ + " to define SCM of your project.");
+ }
+ }
+ }
+
+ private void setProviderIfSupported(String forcedProviderKey) {
+ if (providerPerKey.containsKey(forcedProviderKey)) {
+ this.provider = providerPerKey.get(forcedProviderKey);
+ } else {
+ String supportedProviders = providerPerKey.isEmpty() ? "No SCM provider installed" : ("Supported SCM providers are " + Joiner.on(",").join(providerPerKey.keySet()));
+ throw new IllegalArgumentException("SCM provider was set to \"" + forcedProviderKey + "\" but no SCM provider found for this key. " + supportedProviders);
+ }
+ }
+
+ private void considerOldScmUrl() {
+ if (settings.hasKey(CoreProperties.LINKS_SOURCES_DEV)) {
+ String url = settings.getString(CoreProperties.LINKS_SOURCES_DEV);
+ if (StringUtils.startsWith(url, "scm:")) {
+ String[] split = url.split(":");
+ if (split.length > 1) {
+ setProviderIfSupported(split[1]);
+ }
+ }
+ }
+
+ }
+
+ private void autodetection() {
+ for (ScmProvider installedProvider : providerPerKey.values()) {
+ if (installedProvider.supports(projectReactor.getRoot().getBaseDir())) {
+ if (this.provider == null) {
+ this.provider = installedProvider;
+ } else {
+ throw new IllegalStateException("SCM provider autodetection failed. Both " + this.provider.key() + " and " + installedProvider.key()
+ + " claim to support this project. Please use " + CoreProperties.SCM_PROVIDER_KEY + " to define SCM of your project.");
+ }
+ }
+ }
+ }
+
+ public ScmProvider provider() {
+ return provider;
+ }
+
+ public boolean isDisabled() {
+ return settings.getBoolean(CoreProperties.SCM_DISABLED_KEY);
+ }
+
+ public boolean forceReloadAll() {
+ return settings.getBoolean(FORCE_RELOAD_KEY);
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmSensor.java
new file mode 100644
index 00000000000..078224bb6c6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/ScmSensor.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scm;
+
+import java.util.LinkedList;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Status;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.repository.FileData;
+import org.sonar.batch.repository.ProjectRepositories;
+
+public final class ScmSensor implements Sensor {
+
+ private static final Logger LOG = Loggers.get(ScmSensor.class);
+
+ private final ProjectDefinition projectDefinition;
+ private final ScmConfiguration configuration;
+ private final FileSystem fs;
+ private final ProjectRepositories projectRepositories;
+ private final BatchComponentCache resourceCache;
+ private final ReportPublisher publishReportJob;
+
+ public ScmSensor(ProjectDefinition projectDefinition, ScmConfiguration configuration,
+ ProjectRepositories projectRepositories, FileSystem fs, BatchComponentCache resourceCache, ReportPublisher publishReportJob) {
+ this.projectDefinition = projectDefinition;
+ this.configuration = configuration;
+ this.projectRepositories = projectRepositories;
+ this.fs = fs;
+ this.resourceCache = resourceCache;
+ this.publishReportJob = publishReportJob;
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.name("SCM Sensor");
+ }
+
+ @Override
+ public void execute(SensorContext context) {
+ if (configuration.isDisabled()) {
+ LOG.info("SCM Publisher is disabled");
+ return;
+ }
+ if (configuration.provider() == null) {
+ LOG.info("No SCM system was detected. You can use the '" + CoreProperties.SCM_PROVIDER_KEY + "' property to explicitly specify it.");
+ return;
+ }
+
+ List<InputFile> filesToBlame = collectFilesToBlame();
+ if (!filesToBlame.isEmpty()) {
+ String key = configuration.provider().key();
+ LOG.info("SCM provider for this project is: " + key);
+ DefaultBlameOutput output = new DefaultBlameOutput(publishReportJob.getWriter(), resourceCache, filesToBlame);
+ try {
+ configuration.provider().blameCommand().blame(new DefaultBlameInput(fs, filesToBlame), output);
+ } finally {
+ output.finish();
+ }
+ }
+ }
+
+ private List<InputFile> collectFilesToBlame() {
+ if (configuration.forceReloadAll()) {
+ LOG.warn("Forced reloading of SCM data for all files.");
+ }
+ List<InputFile> filesToBlame = new LinkedList<>();
+ for (InputFile f : fs.inputFiles(fs.predicates().all())) {
+ if (configuration.forceReloadAll() || f.status() != Status.SAME) {
+ addIfNotEmpty(filesToBlame, f);
+ } else {
+ // File status is SAME so that mean fileData exists
+ FileData fileData = projectRepositories.fileData(projectDefinition.getKeyWithBranch(), f.relativePath());
+ if (StringUtils.isEmpty(fileData.revision())) {
+ addIfNotEmpty(filesToBlame, f);
+ }
+ }
+ }
+ return filesToBlame;
+ }
+
+ private static void addIfNotEmpty(List<InputFile> filesToBlame, InputFile f) {
+ if (!f.isEmpty()) {
+ filesToBlame.add(f);
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/package-info.java
new file mode 100644
index 00000000000..925761a7298
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/scm/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.scm;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java
new file mode 100644
index 00000000000..1080dab084a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorContext.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor;
+
+import java.io.Serializable;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputModule;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.coverage.NewCoverage;
+import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage;
+import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
+import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.NewIssue;
+import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
+import org.sonar.api.batch.sensor.measure.NewMeasure;
+import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
+import org.sonar.api.config.Settings;
+import org.sonar.batch.sensor.noop.NoOpNewCpdTokens;
+import org.sonar.batch.sensor.noop.NoOpNewHighlighting;
+
+public class DefaultSensorContext implements SensorContext {
+
+ private static final NoOpNewHighlighting NO_OP_NEW_HIGHLIGHTING = new NoOpNewHighlighting();
+ private static final NoOpNewCpdTokens NO_OP_NEW_CPD_TOKENS = new NoOpNewCpdTokens();
+
+ private final Settings settings;
+ private final FileSystem fs;
+ private final ActiveRules activeRules;
+ private final SensorStorage sensorStorage;
+ private final AnalysisMode analysisMode;
+ private final InputModule module;
+
+ public DefaultSensorContext(InputModule module, Settings settings, FileSystem fs, ActiveRules activeRules, AnalysisMode analysisMode, SensorStorage sensorStorage) {
+ this.module = module;
+ this.settings = settings;
+ this.fs = fs;
+ this.activeRules = activeRules;
+ this.analysisMode = analysisMode;
+ this.sensorStorage = sensorStorage;
+ }
+
+ @Override
+ public Settings settings() {
+ return settings;
+ }
+
+ @Override
+ public FileSystem fileSystem() {
+ return fs;
+ }
+
+ @Override
+ public ActiveRules activeRules() {
+ return activeRules;
+ }
+
+ @Override
+ public InputModule module() {
+ return module;
+ }
+
+ @Override
+ public <G extends Serializable> NewMeasure<G> newMeasure() {
+ return new DefaultMeasure<>(sensorStorage);
+ }
+
+ @Override
+ public NewIssue newIssue() {
+ return new DefaultIssue(sensorStorage);
+ }
+
+ @Override
+ public NewHighlighting newHighlighting() {
+ if (analysisMode.isIssues()) {
+ return NO_OP_NEW_HIGHLIGHTING;
+ }
+ return new DefaultHighlighting(sensorStorage);
+ }
+
+ @Override
+ public NewCoverage newCoverage() {
+ return new DefaultCoverage(sensorStorage);
+ }
+
+ @Override
+ public NewCpdTokens newCpdTokens() {
+ if (analysisMode.isIssues()) {
+ return NO_OP_NEW_CPD_TOKENS;
+ }
+ return new DefaultCpdTokens(sensorStorage);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorStorage.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorStorage.java
new file mode 100644
index 00000000000..62e7d982d34
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/DefaultSensorStorage.java
@@ -0,0 +1,285 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.sensor.coverage.CoverageType;
+import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage;
+import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
+import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.measure.Measure;
+import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.source.Symbol;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.api.utils.SonarException;
+import org.sonar.batch.cpd.DefaultCpdBlockIndexer;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.issue.ModuleIssues;
+import org.sonar.batch.report.ScannerReportUtils;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.batch.sensor.coverage.CoverageExclusions;
+import org.sonar.batch.source.DefaultSymbol;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.internal.pmd.PmdBlockChunker;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+public class DefaultSensorStorage implements SensorStorage {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultSensorStorage.class);
+
+ private static final List<Metric> INTERNAL_METRICS = Arrays.<Metric>asList(
+ // Computed by LinesSensor
+ CoreMetrics.LINES);
+
+ private static final List<String> DEPRECATED_METRICS_KEYS = Arrays.<String>asList(
+ CoreMetrics.DEPENDENCY_MATRIX_KEY,
+ CoreMetrics.DIRECTORY_CYCLES_KEY,
+ CoreMetrics.DIRECTORY_EDGES_WEIGHT_KEY,
+ CoreMetrics.DIRECTORY_FEEDBACK_EDGES_KEY,
+ CoreMetrics.DIRECTORY_TANGLE_INDEX_KEY,
+ CoreMetrics.DIRECTORY_TANGLES_KEY,
+ CoreMetrics.FILE_CYCLES_KEY,
+ CoreMetrics.FILE_EDGES_WEIGHT_KEY,
+ CoreMetrics.FILE_FEEDBACK_EDGES_KEY,
+ CoreMetrics.FILE_TANGLE_INDEX_KEY,
+ CoreMetrics.FILE_TANGLES_KEY,
+ CoreMetrics.DUPLICATIONS_DATA_KEY);
+
+ private final MetricFinder metricFinder;
+ private final ModuleIssues moduleIssues;
+ private final CoverageExclusions coverageExclusions;
+ private final BatchComponentCache componentCache;
+ private final ReportPublisher reportPublisher;
+ private final MeasureCache measureCache;
+ private final SonarCpdBlockIndex index;
+ private final Settings settings;
+
+ public DefaultSensorStorage(MetricFinder metricFinder, ModuleIssues moduleIssues,
+ Settings settings, FileSystem fs, ActiveRules activeRules,
+ CoverageExclusions coverageExclusions, BatchComponentCache componentCache, ReportPublisher reportPublisher, MeasureCache measureCache, SonarCpdBlockIndex index) {
+ this.metricFinder = metricFinder;
+ this.moduleIssues = moduleIssues;
+ this.settings = settings;
+ this.coverageExclusions = coverageExclusions;
+ this.componentCache = componentCache;
+ this.reportPublisher = reportPublisher;
+ this.measureCache = measureCache;
+ this.index = index;
+ }
+
+ private Metric findMetricOrFail(String metricKey) {
+ Metric m = (Metric) metricFinder.findByKey(metricKey);
+ if (m == null) {
+ throw new IllegalStateException("Unknow metric with key: " + metricKey);
+ }
+ return m;
+ }
+
+ @Override
+ public void store(Measure newMeasure) {
+ DefaultMeasure<?> measure = (DefaultMeasure<?>) newMeasure;
+ org.sonar.api.measures.Metric m = findMetricOrFail(measure.metric().key());
+ org.sonar.api.measures.Measure measureToSave = new org.sonar.api.measures.Measure(m);
+ setValueAccordingToMetricType(newMeasure, m, measureToSave);
+ measureToSave.setFromCore(measure.isFromCore());
+ InputComponent inputComponent = newMeasure.inputComponent();
+ Resource resource = componentCache.get(inputComponent).resource();
+ if (coverageExclusions.accept(resource, measureToSave)) {
+ saveMeasure(resource, measureToSave);
+ }
+ }
+
+ public org.sonar.api.measures.Measure saveMeasure(Resource resource, org.sonar.api.measures.Measure measure) {
+ if (DEPRECATED_METRICS_KEYS.contains(measure.getMetricKey())) {
+ // Ignore deprecated metrics
+ return null;
+ }
+ org.sonar.api.batch.measure.Metric metric = metricFinder.findByKey(measure.getMetricKey());
+ if (metric == null) {
+ throw new SonarException("Unknown metric: " + measure.getMetricKey());
+ }
+ if (!measure.isFromCore() && INTERNAL_METRICS.contains(metric)) {
+ LOG.debug("Metric " + metric.key() + " is an internal metric computed by SonarQube. Provided value is ignored.");
+ return measure;
+ }
+ if (measureCache.contains(resource, measure)) {
+ throw new SonarException("Can not add the same measure twice on " + resource + ": " + measure);
+ }
+ measureCache.put(resource, measure);
+ return measure;
+ }
+
+ private void setValueAccordingToMetricType(Measure<?> measure, org.sonar.api.measures.Metric<?> m, org.sonar.api.measures.Measure measureToSave) {
+ switch (m.getType()) {
+ case BOOL:
+ measureToSave.setValue(Boolean.TRUE.equals(measure.value()) ? 1.0 : 0.0);
+ break;
+ case INT:
+ case MILLISEC:
+ case WORK_DUR:
+ case FLOAT:
+ case PERCENT:
+ case RATING:
+ measureToSave.setValue(((Number) measure.value()).doubleValue());
+ break;
+ case STRING:
+ case LEVEL:
+ case DATA:
+ case DISTRIB:
+ measureToSave.setData((String) measure.value());
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported type :" + m.getType());
+ }
+ }
+
+ @Override
+ public void store(Issue issue) {
+ moduleIssues.initAndAddIssue(issue);
+ }
+
+ private File getFile(InputFile file) {
+ BatchComponent r = componentCache.get(file);
+ if (r == null) {
+ throw new IllegalStateException("Provided input file is not indexed");
+ }
+ return (File) r.resource();
+ }
+
+ @Override
+ public void store(DefaultHighlighting highlighting) {
+ ScannerReportWriter writer = reportPublisher.getWriter();
+ DefaultInputFile inputFile = (DefaultInputFile) highlighting.inputFile();
+ writer.writeComponentSyntaxHighlighting(componentCache.get(inputFile).batchId(),
+ Iterables.transform(highlighting.getSyntaxHighlightingRuleSet(), new BuildSyntaxHighlighting()));
+ }
+
+ public void store(DefaultInputFile inputFile, Map<Symbol, Set<TextRange>> referencesBySymbol) {
+ ScannerReportWriter writer = reportPublisher.getWriter();
+ writer.writeComponentSymbols(componentCache.get(inputFile).batchId(),
+ Iterables.transform(referencesBySymbol.entrySet(), new Function<Map.Entry<Symbol, Set<TextRange>>, ScannerReport.Symbol>() {
+ private ScannerReport.Symbol.Builder builder = ScannerReport.Symbol.newBuilder();
+ private ScannerReport.TextRange.Builder rangeBuilder = ScannerReport.TextRange.newBuilder();
+
+ @Override
+ public ScannerReport.Symbol apply(Map.Entry<Symbol, Set<TextRange>> input) {
+ builder.clear();
+ rangeBuilder.clear();
+ DefaultSymbol symbol = (DefaultSymbol) input.getKey();
+ builder.setDeclaration(rangeBuilder.setStartLine(symbol.range().start().line())
+ .setStartOffset(symbol.range().start().lineOffset())
+ .setEndLine(symbol.range().end().line())
+ .setEndOffset(symbol.range().end().lineOffset())
+ .build());
+ for (TextRange reference : input.getValue()) {
+ builder.addReference(rangeBuilder.setStartLine(reference.start().line())
+ .setStartOffset(reference.start().lineOffset())
+ .setEndLine(reference.end().line())
+ .setEndOffset(reference.end().lineOffset())
+ .build());
+ }
+ return builder.build();
+ }
+
+ }));
+ }
+
+ @Override
+ public void store(DefaultCoverage defaultCoverage) {
+ File file = getFile(defaultCoverage.inputFile());
+ if (coverageExclusions.hasMatchingPattern(file)) {
+ return;
+ }
+ CoverageType type = defaultCoverage.type();
+ if (defaultCoverage.linesToCover() > 0) {
+ saveMeasure(file, new org.sonar.api.measures.Measure(type.linesToCover(), (double) defaultCoverage.linesToCover()));
+ saveMeasure(file, new org.sonar.api.measures.Measure(type.uncoveredLines(), (double) (defaultCoverage.linesToCover() - defaultCoverage.coveredLines())));
+ saveMeasure(file, new org.sonar.api.measures.Measure(type.lineHitsData()).setData(KeyValueFormat.format(defaultCoverage.hitsByLine())));
+ }
+ if (defaultCoverage.conditions() > 0) {
+ saveMeasure(file, new org.sonar.api.measures.Measure(type.conditionsToCover(), (double) defaultCoverage.conditions()));
+ saveMeasure(file, new org.sonar.api.measures.Measure(type.uncoveredConditions(), (double) (defaultCoverage.conditions() - defaultCoverage.coveredConditions())));
+ saveMeasure(file, new org.sonar.api.measures.Measure(type.coveredConditionsByLine()).setData(KeyValueFormat.format(defaultCoverage.coveredConditionsByLine())));
+ saveMeasure(file, new org.sonar.api.measures.Measure(type.conditionsByLine()).setData(KeyValueFormat.format(defaultCoverage.conditionsByLine())));
+ }
+ }
+
+ private static class BuildSyntaxHighlighting implements Function<SyntaxHighlightingRule, ScannerReport.SyntaxHighlighting> {
+ private ScannerReport.SyntaxHighlighting.Builder builder = ScannerReport.SyntaxHighlighting.newBuilder();
+ private ScannerReport.TextRange.Builder rangeBuilder = ScannerReport.TextRange.newBuilder();
+
+ @Override
+ public ScannerReport.SyntaxHighlighting apply(@Nonnull SyntaxHighlightingRule input) {
+ builder.setRange(rangeBuilder.setStartLine(input.range().start().line())
+ .setStartOffset(input.range().start().lineOffset())
+ .setEndLine(input.range().end().line())
+ .setEndOffset(input.range().end().lineOffset())
+ .build());
+ builder.setType(ScannerReportUtils.toProtocolType(input.getTextType()));
+ return builder.build();
+ }
+ }
+
+ @Override
+ public void store(DefaultCpdTokens defaultCpdTokens) {
+ InputFile inputFile = defaultCpdTokens.inputFile();
+ PmdBlockChunker blockChunker = new PmdBlockChunker(getBlockSize(inputFile.language()));
+ List<Block> blocks = blockChunker.chunk(inputFile.key(), defaultCpdTokens.getTokenLines());
+ index.insert(inputFile, blocks);
+ }
+
+ @VisibleForTesting
+ int getBlockSize(String languageKey) {
+ int blockSize = settings.getInt("sonar.cpd." + languageKey + ".minimumLines");
+ if (blockSize == 0) {
+ blockSize = DefaultCpdBlockIndexer.getDefaultBlockSize(languageKey);
+ }
+ return blockSize;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorOptimizer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorOptimizer.java
new file mode 100644
index 00000000000..7e8dd63e93a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorOptimizer.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor;
+import org.sonar.api.config.Settings;
+
+@BatchSide
+public class SensorOptimizer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SensorOptimizer.class);
+
+ private final FileSystem fs;
+ private final ActiveRules activeRules;
+ private final Settings settings;
+
+ public SensorOptimizer(FileSystem fs, ActiveRules activeRules, Settings settings) {
+ this.fs = fs;
+ this.activeRules = activeRules;
+ this.settings = settings;
+ }
+
+ /**
+ * Decide if the given Sensor should be executed.
+ */
+ public boolean shouldExecute(DefaultSensorDescriptor descriptor) {
+ if (!fsCondition(descriptor)) {
+ LOG.debug("'{}' skipped because there is no related file in current project", descriptor.name());
+ return false;
+ }
+ if (!activeRulesCondition(descriptor)) {
+ LOG.debug("'{}' skipped because there is no related rule activated in the quality profile", descriptor.name());
+ return false;
+ }
+ if (!settingsCondition(descriptor)) {
+ LOG.debug("'{}' skipped because one of the required properties is missing", descriptor.name());
+ return false;
+ }
+ return true;
+ }
+
+ private boolean settingsCondition(DefaultSensorDescriptor descriptor) {
+ if (!descriptor.properties().isEmpty()) {
+ for (String propertyKey : descriptor.properties()) {
+ if (!settings.hasKey(propertyKey)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private boolean activeRulesCondition(DefaultSensorDescriptor descriptor) {
+ if (!descriptor.ruleRepositories().isEmpty()) {
+ for (String repoKey : descriptor.ruleRepositories()) {
+ if (!activeRules.findByRepository(repoKey).isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private boolean fsCondition(DefaultSensorDescriptor descriptor) {
+ if (!descriptor.languages().isEmpty() || descriptor.type() != null) {
+ FilePredicate langPredicate = descriptor.languages().isEmpty() ? fs.predicates().all() : fs.predicates().hasLanguages(descriptor.languages());
+
+ FilePredicate typePredicate = descriptor.type() == null ? fs.predicates().all() : fs.predicates().hasType(descriptor.type());
+ return fs.hasFiles(fs.predicates().and(langPredicate, typePredicate));
+ }
+ return true;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorWrapper.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorWrapper.java
new file mode 100644
index 00000000000..3e82a222546
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/SensorWrapper.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor;
+import org.sonar.api.resources.Project;
+
+public class SensorWrapper implements org.sonar.api.batch.Sensor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SensorWrapper.class);
+
+ private Sensor wrappedSensor;
+ private SensorContext adaptor;
+ private DefaultSensorDescriptor descriptor;
+ private SensorOptimizer optimizer;
+
+ public SensorWrapper(Sensor newSensor, SensorContext adaptor, SensorOptimizer optimizer) {
+ this.wrappedSensor = newSensor;
+ this.optimizer = optimizer;
+ descriptor = new DefaultSensorDescriptor();
+ newSensor.describe(descriptor);
+ this.adaptor = adaptor;
+ }
+
+ public Sensor wrappedSensor() {
+ return wrappedSensor;
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return optimizer.shouldExecute(descriptor);
+ }
+
+ @Override
+ public void analyse(Project module, org.sonar.api.batch.SensorContext context) {
+ wrappedSensor.execute(adaptor);
+ }
+
+ @Override
+ public String toString() {
+ return descriptor.name() + (LOG.isDebugEnabled() ? " (wrapped)" : "");
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageConstants.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageConstants.java
new file mode 100644
index 00000000000..569ac157e42
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageConstants.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor.coverage;
+
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+
+import java.util.Collection;
+
+public class CoverageConstants {
+
+ public static final Collection<Metric> COVERAGE_METRICS = ImmutableList.<Metric>of(CoreMetrics.LINES_TO_COVER, CoreMetrics.UNCOVERED_LINES, CoreMetrics.NEW_LINES_TO_COVER,
+ CoreMetrics.NEW_UNCOVERED_LINES, CoreMetrics.CONDITIONS_TO_COVER, CoreMetrics.UNCOVERED_CONDITIONS,
+ CoreMetrics.NEW_CONDITIONS_TO_COVER, CoreMetrics.NEW_UNCOVERED_CONDITIONS);
+
+ public static final Collection<Metric> LINE_COVERAGE_METRICS = ImmutableList.<Metric>of(CoreMetrics.UNCOVERED_LINES, CoreMetrics.LINES_TO_COVER, CoreMetrics.NEW_UNCOVERED_LINES,
+ CoreMetrics.NEW_LINES_TO_COVER);
+
+ public static final Collection<Metric> BRANCH_COVERAGE_METRICS = ImmutableList.<Metric>of(CoreMetrics.UNCOVERED_CONDITIONS, CoreMetrics.CONDITIONS_TO_COVER,
+ CoreMetrics.NEW_UNCOVERED_CONDITIONS, CoreMetrics.NEW_CONDITIONS_TO_COVER);
+
+ private CoverageConstants() {
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageExclusions.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageExclusions.java
new file mode 100644
index 00000000000..198b61141bb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/CoverageExclusions.java
@@ -0,0 +1,210 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor.coverage;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.api.utils.WildcardPattern;
+
+public class CoverageExclusions {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CoverageExclusions.class);
+
+ private final Settings settings;
+ private final Set<Metric> coverageMetrics;
+ private final Set<Metric> byLineMetrics;
+ private Collection<WildcardPattern> resourcePatterns;
+
+ private final FileSystem fs;
+
+ public CoverageExclusions(Settings settings, FileSystem fs) {
+ this.settings = settings;
+ this.fs = fs;
+ this.coverageMetrics = new HashSet<>();
+ this.byLineMetrics = new HashSet<>();
+ // UT
+ coverageMetrics.add(CoreMetrics.COVERAGE);
+ coverageMetrics.add(CoreMetrics.LINE_COVERAGE);
+ coverageMetrics.add(CoreMetrics.BRANCH_COVERAGE);
+ coverageMetrics.add(CoreMetrics.UNCOVERED_LINES);
+ coverageMetrics.add(CoreMetrics.LINES_TO_COVER);
+ coverageMetrics.add(CoreMetrics.UNCOVERED_CONDITIONS);
+ coverageMetrics.add(CoreMetrics.CONDITIONS_TO_COVER);
+ coverageMetrics.add(CoreMetrics.CONDITIONS_BY_LINE);
+ coverageMetrics.add(CoreMetrics.COVERED_CONDITIONS_BY_LINE);
+ coverageMetrics.add(CoreMetrics.COVERAGE_LINE_HITS_DATA);
+ coverageMetrics.add(CoreMetrics.NEW_LINES_TO_COVER);
+ coverageMetrics.add(CoreMetrics.NEW_UNCOVERED_LINES);
+ coverageMetrics.add(CoreMetrics.NEW_UNCOVERED_CONDITIONS);
+ // IT
+ coverageMetrics.add(CoreMetrics.IT_COVERAGE);
+ coverageMetrics.add(CoreMetrics.IT_LINE_COVERAGE);
+ coverageMetrics.add(CoreMetrics.IT_BRANCH_COVERAGE);
+ coverageMetrics.add(CoreMetrics.IT_UNCOVERED_LINES);
+ coverageMetrics.add(CoreMetrics.IT_LINES_TO_COVER);
+ coverageMetrics.add(CoreMetrics.IT_UNCOVERED_CONDITIONS);
+ coverageMetrics.add(CoreMetrics.IT_CONDITIONS_TO_COVER);
+ coverageMetrics.add(CoreMetrics.IT_CONDITIONS_BY_LINE);
+ coverageMetrics.add(CoreMetrics.IT_COVERED_CONDITIONS_BY_LINE);
+ coverageMetrics.add(CoreMetrics.IT_COVERAGE_LINE_HITS_DATA);
+ coverageMetrics.add(CoreMetrics.NEW_IT_LINES_TO_COVER);
+ coverageMetrics.add(CoreMetrics.NEW_IT_UNCOVERED_LINES);
+ coverageMetrics.add(CoreMetrics.NEW_IT_UNCOVERED_CONDITIONS);
+ // OVERALL
+ coverageMetrics.add(CoreMetrics.OVERALL_COVERAGE);
+ coverageMetrics.add(CoreMetrics.OVERALL_LINE_COVERAGE);
+ coverageMetrics.add(CoreMetrics.OVERALL_BRANCH_COVERAGE);
+ coverageMetrics.add(CoreMetrics.OVERALL_UNCOVERED_LINES);
+ coverageMetrics.add(CoreMetrics.OVERALL_LINES_TO_COVER);
+ coverageMetrics.add(CoreMetrics.OVERALL_UNCOVERED_CONDITIONS);
+ coverageMetrics.add(CoreMetrics.OVERALL_CONDITIONS_TO_COVER);
+ coverageMetrics.add(CoreMetrics.OVERALL_CONDITIONS_BY_LINE);
+ coverageMetrics.add(CoreMetrics.OVERALL_COVERED_CONDITIONS_BY_LINE);
+ coverageMetrics.add(CoreMetrics.OVERALL_COVERAGE_LINE_HITS_DATA);
+ coverageMetrics.add(CoreMetrics.NEW_OVERALL_LINES_TO_COVER);
+ coverageMetrics.add(CoreMetrics.NEW_OVERALL_UNCOVERED_LINES);
+ coverageMetrics.add(CoreMetrics.NEW_OVERALL_UNCOVERED_CONDITIONS);
+
+ byLineMetrics.add(CoreMetrics.OVERALL_COVERAGE_LINE_HITS_DATA);
+ byLineMetrics.add(CoreMetrics.OVERALL_CONDITIONS_BY_LINE);
+ byLineMetrics.add(CoreMetrics.OVERALL_COVERED_CONDITIONS_BY_LINE);
+ byLineMetrics.add(CoreMetrics.COVERAGE_LINE_HITS_DATA);
+ byLineMetrics.add(CoreMetrics.COVERED_CONDITIONS_BY_LINE);
+ byLineMetrics.add(CoreMetrics.CONDITIONS_BY_LINE);
+ byLineMetrics.add(CoreMetrics.IT_COVERAGE_LINE_HITS_DATA);
+ byLineMetrics.add(CoreMetrics.IT_CONDITIONS_BY_LINE);
+ byLineMetrics.add(CoreMetrics.IT_COVERED_CONDITIONS_BY_LINE);
+
+ initPatterns();
+ }
+
+ private boolean isLineMetrics(Metric<?> metric) {
+ return this.byLineMetrics.contains(metric);
+ }
+
+ public void validate(Measure<?> measure, InputFile inputFile) {
+ Metric<?> metric = measure.getMetric();
+
+ if (!isLineMetrics(metric)) {
+ return;
+ }
+
+ Map<Integer, Integer> m = KeyValueFormat.parseIntInt(measure.getData());
+ validatePositiveLine(m, inputFile.absolutePath());
+ validateMaxLine(m, inputFile);
+ }
+
+ @CheckForNull
+ private InputFile getInputFile(String filePath) {
+ return fs.inputFile(fs.predicates().hasRelativePath(filePath));
+ }
+
+ public void validate(Measure<?> measure, String filePath) {
+ Metric<?> metric = measure.getMetric();
+
+ if (!isLineMetrics(metric)) {
+ return;
+ }
+
+ InputFile inputFile = getInputFile(filePath);
+
+ if (inputFile == null) {
+ throw new IllegalStateException(String.format("Can't create measure for resource '%s': resource is not indexed as a file", filePath));
+ }
+
+ validate(measure, inputFile);
+ }
+
+ private static void validateMaxLine(Map<Integer, Integer> m, InputFile inputFile) {
+ int maxLine = inputFile.lines();
+
+ for (int l : m.keySet()) {
+ if (l > maxLine) {
+ throw new IllegalStateException(String.format("Can't create measure for line %d for file '%s' with %d lines", l, inputFile.absolutePath(), maxLine));
+ }
+ }
+ }
+
+ private static void validatePositiveLine(Map<Integer, Integer> m, String filePath) {
+ for (int l : m.keySet()) {
+ if (l <= 0) {
+ throw new IllegalStateException(String.format("Measure with line %d for file '%s' must be > 0", l, filePath));
+ }
+ }
+ }
+
+ public boolean accept(Resource resource, Measure<?> measure) {
+ if (isCoverageMetric(measure.getMetric())) {
+ return !hasMatchingPattern(resource);
+ } else {
+ return true;
+ }
+ }
+
+ private boolean isCoverageMetric(Metric<?> metric) {
+ return this.coverageMetrics.contains(metric);
+ }
+
+ public boolean hasMatchingPattern(Resource resource) {
+ boolean found = false;
+ Iterator<WildcardPattern> iterator = resourcePatterns.iterator();
+ while (!found && iterator.hasNext()) {
+ found = resource.matchFilePattern(iterator.next().toString());
+ }
+ return found;
+ }
+
+ @VisibleForTesting
+ final void initPatterns() {
+ Builder<WildcardPattern> builder = ImmutableList.builder();
+ for (String pattern : settings.getStringArray(CoreProperties.PROJECT_COVERAGE_EXCLUSIONS_PROPERTY)) {
+ builder.add(WildcardPattern.create(pattern));
+ }
+ resourcePatterns = builder.build();
+ log("Excluded sources for coverage: ", resourcePatterns);
+ }
+
+ private static void log(String title, Collection<WildcardPattern> patterns) {
+ if (!patterns.isEmpty()) {
+ LOG.info(title);
+ for (WildcardPattern pattern : patterns) {
+ LOG.info(" " + pattern);
+ }
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/package-info.java
new file mode 100644
index 00000000000..b4c6be3d144
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/coverage/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.batch.sensor.coverage;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewCpdTokens.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewCpdTokens.java
new file mode 100644
index 00000000000..afd00476b2a
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewCpdTokens.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor.noop;
+
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
+
+public class NoOpNewCpdTokens implements NewCpdTokens {
+ @Override
+ public void save() {
+ // Do nothing
+ }
+
+ @Override
+ public NoOpNewCpdTokens onFile(InputFile inputFile) {
+ // Do nothing
+ return this;
+ }
+
+ @Override
+ public NewCpdTokens addToken(TextRange range, String image) {
+ // Do nothing
+ return this;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewHighlighting.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewHighlighting.java
new file mode 100644
index 00000000000..7ead32f7f04
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/NoOpNewHighlighting.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor.noop;
+
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+
+public class NoOpNewHighlighting implements NewHighlighting {
+ @Override
+ public void save() {
+ // Do nothing
+ }
+
+ @Override
+ public NewHighlighting onFile(InputFile inputFile) {
+ // Do nothing
+ return this;
+ }
+
+ @Override
+ public NewHighlighting highlight(int startOffset, int endOffset, TypeOfText typeOfText) {
+ // Do nothing
+ return this;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/package-info.java
new file mode 100644
index 00000000000..0a654a40800
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/noop/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.batch.sensor.noop;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/package-info.java
new file mode 100644
index 00000000000..4c9f457a97f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/sensor/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.batch.sensor;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizerSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizerSensor.java
new file mode 100644
index 00000000000..616ac48ffef
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizerSensor.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.sonar.api.batch.Phase;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+
+@Phase(name = Phase.Name.POST)
+public final class CodeColorizerSensor implements Sensor {
+
+ private final ReportPublisher reportPublisher;
+ private final BatchComponentCache resourceCache;
+ private final CodeColorizers codeColorizers;
+
+ public CodeColorizerSensor(ReportPublisher reportPublisher, BatchComponentCache resourceCache, CodeColorizers codeColorizers) {
+ this.reportPublisher = reportPublisher;
+ this.resourceCache = resourceCache;
+ this.codeColorizers = codeColorizers;
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.name("Code Colorizer Sensor");
+ }
+
+ @Override
+ public void execute(final SensorContext context) {
+ FileSystem fs = context.fileSystem();
+ for (InputFile f : fs.inputFiles(fs.predicates().all())) {
+ ScannerReportReader reader = new ScannerReportReader(reportPublisher.getReportDir());
+ int batchId = resourceCache.get(f).batchId();
+ String language = f.language();
+ if (reader.hasSyntaxHighlighting(batchId) || language == null) {
+ continue;
+ }
+ codeColorizers.toSyntaxHighlighting(f.file(), fs.encoding(), language, context.newHighlighting().onFile(f));
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizers.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizers.java
new file mode 100644
index 00000000000..39a4a0b1c98
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/CodeColorizers.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import com.google.common.collect.Lists;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.apache.commons.io.input.BOMInputStream;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.web.CodeColorizerFormat;
+import org.sonar.colorizer.JavaTokenizers;
+import org.sonar.colorizer.Tokenizer;
+
+/**
+ * Central point for sonar-colorizer extensions
+ */
+@BatchSide
+public class CodeColorizers {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CodeColorizers.class);
+
+ private final Map<String, CodeColorizerFormat> byLang;
+
+ public CodeColorizers(List<CodeColorizerFormat> formats) {
+ byLang = new HashMap<>();
+ for (CodeColorizerFormat format : formats) {
+ byLang.put(format.getLanguageKey(), format);
+ }
+
+ LOG.debug("Code colorizer, supported languages: " + StringUtils.join(byLang.keySet(), ","));
+ }
+
+ /**
+ * Used when no plugin is defining some CodeColorizerFormat
+ */
+ public CodeColorizers() {
+ this(Lists.<CodeColorizerFormat>newArrayList());
+ }
+
+ @CheckForNull
+ public void toSyntaxHighlighting(File file, Charset charset, String language, NewHighlighting highlighting) {
+ CodeColorizerFormat format = byLang.get(language);
+ List<Tokenizer> tokenizers;
+ if (format == null) {
+ // Workaround for Java test code since Java plugin only provides highlighting for main source and no colorizer
+ // TODO can be dropped when Java plugin embed its own CodeColorizerFormat of (better) provides highlighting for tests
+ // See SONARJAVA-830
+ if ("java".equals(language)) {
+ tokenizers = JavaTokenizers.forHtml();
+ } else {
+ return;
+ }
+ } else {
+ tokenizers = format.getTokenizers();
+ }
+ try (Reader reader = new BufferedReader(new InputStreamReader(new BOMInputStream(new FileInputStream(file)), charset))) {
+ new HighlightingRenderer().render(reader, tokenizers, highlighting);
+ } catch (Exception e) {
+ LOG.warn("Unable to perform colorization of file " + file, e);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultHighlightable.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultHighlightable.java
new file mode 100644
index 00000000000..80403efc298
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultHighlightable.java
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.source.Highlightable;
+
+/**
+ * @since 3.6
+ */
+public class DefaultHighlightable implements Highlightable {
+
+ private static final HighlightingBuilder NO_OP_BUILDER = new NoOpHighlightingBuilder();
+ private final DefaultInputFile inputFile;
+ private final SensorStorage sensorStorage;
+ private final AnalysisMode analysisMode;
+
+ public DefaultHighlightable(DefaultInputFile inputFile, SensorStorage sensorStorage, AnalysisMode analysisMode) {
+ this.inputFile = inputFile;
+ this.sensorStorage = sensorStorage;
+ this.analysisMode = analysisMode;
+ }
+
+ @Override
+ public HighlightingBuilder newHighlighting() {
+ if (analysisMode.isIssues()) {
+ return NO_OP_BUILDER;
+ }
+ DefaultHighlighting defaultHighlighting = new DefaultHighlighting(sensorStorage);
+ defaultHighlighting.onFile(inputFile);
+ return new DefaultHighlightingBuilder(defaultHighlighting);
+ }
+
+ private static final class NoOpHighlightingBuilder implements HighlightingBuilder {
+ @Override
+ public HighlightingBuilder highlight(int startOffset, int endOffset, String typeOfText) {
+ // Do nothing
+ return this;
+ }
+
+ @Override
+ public void done() {
+ // Do nothing
+ }
+ }
+
+ private static class DefaultHighlightingBuilder implements HighlightingBuilder {
+
+ private final DefaultHighlighting defaultHighlighting;
+
+ public DefaultHighlightingBuilder(DefaultHighlighting defaultHighlighting) {
+ this.defaultHighlighting = defaultHighlighting;
+ }
+
+ @Override
+ public HighlightingBuilder highlight(int startOffset, int endOffset, String typeOfText) {
+ TypeOfText type = org.sonar.api.batch.sensor.highlighting.TypeOfText.forCssClass(typeOfText);
+ defaultHighlighting.highlight(startOffset, endOffset, type);
+ return this;
+ }
+
+ @Override
+ public void done() {
+ defaultHighlighting.save();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbol.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbol.java
new file mode 100644
index 00000000000..a317bfb2de8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbol.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import com.google.common.base.Objects;
+import org.sonar.api.batch.fs.TextRange;
+
+import java.io.Serializable;
+
+public class DefaultSymbol implements org.sonar.api.source.Symbol, Serializable {
+
+ private TextRange range;
+ private int length;
+
+ public DefaultSymbol(TextRange range, int length) {
+ this.range = range;
+ this.length = length;
+ }
+
+ @Override
+ public int getDeclarationStartOffset() {
+ throw new UnsupportedOperationException("getDeclarationStartOffset");
+ }
+
+ @Override
+ public int getDeclarationEndOffset() {
+ throw new UnsupportedOperationException("getDeclarationEndOffset");
+ }
+
+ @Override
+ public String getFullyQualifiedName() {
+ throw new UnsupportedOperationException("getFullyQualifiedName");
+ }
+
+ public TextRange range() {
+ return range;
+ }
+
+ public int getLength() {
+ return length;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper("Symbol")
+ .add("range", range)
+ .toString();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolTable.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolTable.java
new file mode 100644
index 00000000000..f4ecc8e3761
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolTable.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.source.Symbol;
+import org.sonar.api.source.Symbolizable;
+
+public class DefaultSymbolTable implements Symbolizable.SymbolTable {
+
+ private Map<Symbol, Set<TextRange>> referencesBySymbol;
+
+ private DefaultSymbolTable(Map<Symbol, Set<TextRange>> referencesBySymbol) {
+ this.referencesBySymbol = referencesBySymbol;
+ }
+
+ public Map<Symbol, Set<TextRange>> getReferencesBySymbol() {
+ return referencesBySymbol;
+ }
+
+ @Override
+ public List<Symbol> symbols() {
+ List<Symbol> result = new ArrayList<>();
+ for (Symbol symbol : referencesBySymbol.keySet()) {
+ result.add((Symbol) symbol);
+ }
+ return result;
+ }
+
+ @Override
+ public List<Integer> references(Symbol symbol) {
+ throw new UnsupportedOperationException("references");
+ }
+
+ public static class Builder implements Symbolizable.SymbolTableBuilder {
+
+ private static final class FakeSymbol implements Symbol {
+ @Override
+ public String getFullyQualifiedName() {
+ return null;
+ }
+
+ @Override
+ public int getDeclarationStartOffset() {
+ return 0;
+ }
+
+ @Override
+ public int getDeclarationEndOffset() {
+ return 0;
+ }
+ }
+
+ private final Map<Symbol, Set<TextRange>> referencesBySymbol = new LinkedHashMap<>();
+ private final DefaultInputFile inputFile;
+
+ public Builder(DefaultInputFile inputFile) {
+ this.inputFile = inputFile;
+ }
+
+ @Override
+ public Symbol newSymbol(int fromOffset, int toOffset) {
+ TextRange declarationRange = inputFile.newRange(fromOffset, toOffset);
+ DefaultSymbol symbol = new DefaultSymbol(declarationRange, toOffset - fromOffset);
+ referencesBySymbol.put(symbol, new TreeSet<>(new Comparator<TextRange>() {
+ @Override
+ public int compare(TextRange o1, TextRange o2) {
+ return o1.start().compareTo(o2.start());
+ }
+ }));
+ return symbol;
+ }
+
+ @Override
+ public void newReference(Symbol symbol, int fromOffset) {
+ newReference(symbol, fromOffset, fromOffset + ((DefaultSymbol) symbol).getLength());
+ }
+
+ @Override
+ public void newReference(Symbol symbol, int fromOffset, int toOffset) {
+ if (!referencesBySymbol.containsKey(symbol)) {
+ throw new UnsupportedOperationException("Cannot add reference to a symbol in another file");
+ }
+ TextRange referenceRange = inputFile.newRange(fromOffset, toOffset);
+
+ if (referenceRange.overlap(((DefaultSymbol) symbol).range())) {
+ throw new UnsupportedOperationException("Cannot add reference (" + fromOffset + ") overlapping " + symbol + " in " + inputFile.key());
+ }
+ referencesBySymbol.get(symbol).add(referenceRange);
+ }
+
+ @Override
+ public Symbolizable.SymbolTable build() {
+ return new DefaultSymbolTable(referencesBySymbol);
+ }
+
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolizable.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolizable.java
new file mode 100644
index 00000000000..cddeabb9683
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/DefaultSymbolizable.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import java.util.Collections;
+import java.util.List;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.source.Symbol;
+import org.sonar.api.source.Symbolizable;
+import org.sonar.batch.sensor.DefaultSensorStorage;
+
+public class DefaultSymbolizable implements Symbolizable {
+
+ private static final NoOpSymbolTableBuilder NO_OP_SYMBOL_TABLE_BUILDER = new NoOpSymbolTableBuilder();
+ private static final NoOpSymbolTable NO_OP_SYMBOL_TABLE = new NoOpSymbolTable();
+ private static final NoOpSymbol NO_OP_SYMBOL = new NoOpSymbol();
+
+ private static final class NoOpSymbolTableBuilder implements SymbolTableBuilder {
+ @Override
+ public Symbol newSymbol(int fromOffset, int toOffset) {
+ return NO_OP_SYMBOL;
+ }
+
+ @Override
+ public void newReference(Symbol symbol, int fromOffset) {
+ // Do nothing
+ }
+
+ @Override
+ public void newReference(Symbol symbol, int fromOffset, int toOffset) {
+ // Do nothing
+ }
+
+ @Override
+ public SymbolTable build() {
+ return NO_OP_SYMBOL_TABLE;
+ }
+ }
+
+ private static final class NoOpSymbolTable implements SymbolTable {
+ @Override
+ public List<Symbol> symbols() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<Integer> references(Symbol symbol) {
+ return Collections.emptyList();
+ }
+ }
+
+ private static final class NoOpSymbol implements Symbol {
+ @Override
+ public String getFullyQualifiedName() {
+ return null;
+ }
+
+ @Override
+ public int getDeclarationStartOffset() {
+ return 0;
+ }
+
+ @Override
+ public int getDeclarationEndOffset() {
+ return 0;
+ }
+ }
+
+ private final DefaultInputFile inputFile;
+ private final DefaultSensorStorage sensorStorage;
+ private final AnalysisMode analysisMode;
+
+ public DefaultSymbolizable(DefaultInputFile inputFile, DefaultSensorStorage sensorStorage, AnalysisMode analysisMode) {
+ this.inputFile = inputFile;
+ this.sensorStorage = sensorStorage;
+ this.analysisMode = analysisMode;
+ }
+
+ @Override
+ public SymbolTableBuilder newSymbolTableBuilder() {
+ if (analysisMode.isIssues()) {
+ return NO_OP_SYMBOL_TABLE_BUILDER;
+ }
+ return new DefaultSymbolTable.Builder(inputFile);
+ }
+
+ @Override
+ public void setSymbolTable(SymbolTable symbolTable) {
+ if (analysisMode.isIssues()) {
+ // No need for symbols in issues mode
+ return;
+ }
+ sensorStorage.store(inputFile, ((DefaultSymbolTable) symbolTable).getReferencesBySymbol());
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightableBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightableBuilder.java
new file mode 100644
index 00000000000..d911516b984
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightableBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.source.Highlightable;
+import org.sonar.batch.deprecated.perspectives.PerspectiveBuilder;
+import org.sonar.batch.index.BatchComponent;
+
+public class HighlightableBuilder extends PerspectiveBuilder<Highlightable> {
+
+ private final SensorStorage sensorStorage;
+ private final AnalysisMode analysisMode;
+
+ public HighlightableBuilder(SensorStorage sensorStorage, AnalysisMode analysisMode) {
+ super(Highlightable.class);
+ this.sensorStorage = sensorStorage;
+ this.analysisMode = analysisMode;
+ }
+
+ @CheckForNull
+ @Override
+ public Highlightable loadPerspective(Class<Highlightable> perspectiveClass, BatchComponent component) {
+ if (component.isFile()) {
+ InputFile path = (InputFile) component.inputComponent();
+ return new DefaultHighlightable((DefaultInputFile) path, sensorStorage, analysisMode);
+ }
+ return null;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingCodeBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingCodeBuilder.java
new file mode 100644
index 00000000000..b807ee2d5e2
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingCodeBuilder.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.colorizer.HtmlCodeBuilder;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HighlightingCodeBuilder extends HtmlCodeBuilder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HighlightingCodeBuilder.class);
+
+ private int currentOffset = 0;
+ private static final Pattern START_TAG_PATTERN = Pattern.compile("<span class=\"(.+)\">");
+ private static final Pattern END_TAG_PATTERN = Pattern.compile("</span>");
+ private int startOffset = -1;
+ private String cssClass;
+ private final NewHighlighting highlighting;
+
+ public HighlightingCodeBuilder(NewHighlighting highlighting) {
+ this.highlighting = highlighting;
+ }
+
+ @Override
+ public Appendable append(CharSequence csq) {
+ for (int i = 0; i < csq.length(); i++) {
+ append(csq.charAt(i));
+ }
+ return this;
+ }
+
+ @Override
+ public Appendable append(char c) {
+ currentOffset++;
+ return this;
+ }
+
+ @Override
+ public void appendWithoutTransforming(String htmlTag) {
+ if (startOffset == -1) {
+ Matcher startMatcher = START_TAG_PATTERN.matcher(htmlTag);
+ if (startMatcher.matches()) {
+ startOffset = currentOffset;
+ cssClass = startMatcher.group(1);
+ } else {
+ LOG.warn("Expected to match highlighting start html tag but was: " + htmlTag);
+ }
+ } else {
+ Matcher endMatcher = END_TAG_PATTERN.matcher(htmlTag);
+ if (endMatcher.matches()) {
+ highlighting.highlight(startOffset, currentOffset, TypeOfText.forCssClass(cssClass));
+ startOffset = -1;
+ } else {
+ LOG.warn("Expected to match highlighting end html tag but was: " + htmlTag);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public StringBuilder getColorizedCode() {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingRenderer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingRenderer.java
new file mode 100644
index 00000000000..f4b9f3d399f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/HighlightingRenderer.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.channel.Channel;
+import org.sonar.channel.CodeReader;
+import org.sonar.colorizer.HtmlCodeBuilder;
+import org.sonar.colorizer.TokenizerDispatcher;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class HighlightingRenderer {
+
+ public void render(Reader code, List<? extends Channel<HtmlCodeBuilder>> tokenizers, NewHighlighting highlighting) {
+ List<Channel<HtmlCodeBuilder>> allTokenizers = new ArrayList<>();
+ HighlightingCodeBuilder codeBuilder = new HighlightingCodeBuilder(highlighting);
+
+ allTokenizers.addAll(tokenizers);
+
+ new TokenizerDispatcher(allTokenizers).colorize(new CodeReader(code), codeBuilder);
+ highlighting.save();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/LinesSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/LinesSensor.java
new file mode 100644
index 00000000000..d27c90953a0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/LinesSensor.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.sonar.api.batch.Phase;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
+import org.sonar.api.measures.CoreMetrics;
+
+@Phase(name = Phase.Name.PRE)
+public final class LinesSensor implements Sensor {
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.name("Lines Sensor");
+ }
+
+ @Override
+ public void execute(final SensorContext context) {
+ FileSystem fs = context.fileSystem();
+ for (InputFile f : fs.inputFiles(fs.predicates().hasType(Type.MAIN))) {
+ ((DefaultMeasure<Integer>) context.<Integer>newMeasure()
+ .on(f)
+ .forMetric(CoreMetrics.LINES)
+ .withValue(f.lines()))
+ .setFromCore()
+ .save();
+ if (f.language() == null) {
+ // As an approximation for files with no language plugin we consider every non blank line as ncloc
+ ((DefaultMeasure<Integer>) context.<Integer>newMeasure()
+ .on(f)
+ .forMetric(CoreMetrics.NCLOC)
+ .withValue(((DefaultInputFile) f).nonBlankLines()))
+ .save();
+ // No test and no coverage on those files
+ ((DefaultMeasure<Integer>) context.<Integer>newMeasure()
+ .on(f)
+ .forMetric(CoreMetrics.LINES_TO_COVER)
+ .withValue(0))
+ .save();
+ }
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/SymbolizableBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/SymbolizableBuilder.java
new file mode 100644
index 00000000000..fda6c4c7166
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/SymbolizableBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.source.Symbolizable;
+import org.sonar.batch.deprecated.perspectives.PerspectiveBuilder;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.sensor.DefaultSensorStorage;
+
+public class SymbolizableBuilder extends PerspectiveBuilder<Symbolizable> {
+
+ private final DefaultSensorStorage sensorStorage;
+ private final AnalysisMode analysisMode;
+
+ public SymbolizableBuilder(DefaultSensorStorage sensorStorage, AnalysisMode analysisMode) {
+ super(Symbolizable.class);
+ this.sensorStorage = sensorStorage;
+ this.analysisMode = analysisMode;
+ }
+
+ @CheckForNull
+ @Override
+ public Symbolizable loadPerspective(Class<Symbolizable> perspectiveClass, BatchComponent component) {
+ if (component.isFile()) {
+ InputFile path = (InputFile) component.inputComponent();
+ return new DefaultSymbolizable((DefaultInputFile) path, sensorStorage, analysisMode);
+ }
+ return null;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/ZeroCoverageSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/ZeroCoverageSensor.java
new file mode 100644
index 00000000000..f211050dabc
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/ZeroCoverageSensor.java
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Sets;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.Phase;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.measure.Metric;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.batch.sensor.coverage.CoverageType;
+import org.sonar.api.batch.sensor.coverage.NewCoverage;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.batch.scan.measure.MeasureCache;
+
+import static com.google.common.collect.Iterables.concat;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.collect.Sets.newHashSet;
+
+@Phase(name = Phase.Name.POST)
+public final class ZeroCoverageSensor implements Sensor {
+
+ private static final class MeasureToMetricKey implements Function<Measure, String> {
+ @Override
+ public String apply(Measure input) {
+ return input.getMetricKey();
+ }
+ }
+
+ private static final class MetricToKey implements Function<Metric, String> {
+ @Override
+ public String apply(Metric input) {
+ return input.key();
+ }
+ }
+
+ private final MeasureCache measureCache;
+
+ public ZeroCoverageSensor(MeasureCache measureCache) {
+ this.measureCache = measureCache;
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.name("Zero Coverage Sensor");
+ }
+
+ @Override
+ public void execute(final SensorContext context) {
+ FileSystem fs = context.fileSystem();
+ for (InputFile f : fs.inputFiles(fs.predicates().hasType(Type.MAIN))) {
+ if (!isCoverageMeasuresAlreadyDefined(f)) {
+ Measure execLines = measureCache.byMetric(f.key(), CoreMetrics.EXECUTABLE_LINES_DATA_KEY);
+ if (execLines != null) {
+ storeZeroCoverageForEachExecutableLine(context, f, execLines);
+ }
+
+ }
+ }
+ }
+
+ private static void storeZeroCoverageForEachExecutableLine(final SensorContext context, InputFile f, Measure execLines) {
+ NewCoverage newCoverage = context.newCoverage().ofType(CoverageType.UNIT).onFile(f);
+ Map<Integer, String> lineMeasures = KeyValueFormat.parseIntString((String) execLines.value());
+ for (Map.Entry<Integer, String> lineMeasure : lineMeasures.entrySet()) {
+ int lineIdx = lineMeasure.getKey();
+ if (lineIdx <= f.lines()) {
+ String value = lineMeasure.getValue();
+ if (StringUtils.isNotEmpty(value) && Integer.parseInt(value) > 0) {
+ newCoverage.lineHits(lineIdx, 0);
+ }
+ }
+ }
+ newCoverage.save();
+ }
+
+ private boolean isCoverageMeasuresAlreadyDefined(InputFile f) {
+ Set<String> metricKeys = newHashSet(transform(measureCache.byComponentKey(f.key()), new MeasureToMetricKey()));
+ Function<Metric, String> metricToKey = new MetricToKey();
+ Set<String> allCoverageMetricKeys = newHashSet(concat(transform(CoverageType.UNIT.allMetrics(), metricToKey),
+ transform(CoverageType.IT.allMetrics(), metricToKey),
+ transform(CoverageType.OVERALL.allMetrics(), metricToKey)));
+ return !Sets.intersection(metricKeys, allCoverageMetricKeys).isEmpty();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/source/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/package-info.java
new file mode 100644
index 00000000000..c33229b0ab2
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/source/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.source;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/task/ListTask.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/ListTask.java
new file mode 100644
index 00000000000..7c535674d82
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/ListTask.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.task;
+
+import org.sonar.api.task.Task;
+import org.sonar.api.task.TaskDefinition;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+public class ListTask implements Task {
+
+ private static final Logger LOG = Loggers.get(ListTask.class);
+
+ public static final String KEY = "list";
+
+ public static final TaskDefinition DEFINITION = TaskDefinition.builder()
+ .key(KEY)
+ .description("List available tasks")
+ .taskClass(ListTask.class)
+ .build();
+
+ private final Tasks tasks;
+
+ public ListTask(Tasks tasks) {
+ this.tasks = tasks;
+ }
+
+ @Override
+ public void execute() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\nAvailable tasks:\n");
+ for (TaskDefinition def : tasks.definitions()) {
+ sb.append(" - " + def.key() + ": " + def.description() + "\n");
+ }
+ sb.append("\n");
+ LOG.info(sb.toString());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/task/ScanTask.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/ScanTask.java
new file mode 100644
index 00000000000..c5ba59c3d37
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/ScanTask.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.task;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.task.Task;
+import org.sonar.api.task.TaskDefinition;
+import org.sonar.batch.analysis.AnalysisProperties;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.cache.ProjectSyncContainer;
+import org.sonar.batch.scan.ProjectScanContainer;
+import org.sonar.core.platform.ComponentContainer;
+
+public class ScanTask implements Task {
+ public static final TaskDefinition DEFINITION = TaskDefinition.builder()
+ .description("Scan project")
+ .key(CoreProperties.SCAN_TASK)
+ .taskClass(ScanTask.class)
+ .build();
+
+ private final ComponentContainer taskContainer;
+ private final TaskProperties taskProps;
+
+ public ScanTask(TaskContainer taskContainer, TaskProperties taskProps) {
+ this.taskContainer = taskContainer;
+ this.taskProps = taskProps;
+ }
+
+ @Override
+ public void execute() {
+ AnalysisProperties props = new AnalysisProperties(taskProps.properties(), taskProps.property(CoreProperties.ENCRYPTION_SECRET_KEY_PATH));
+ if (isIssuesMode(props)) {
+ String projectKey = getProjectKeyWithBranch(props);
+ new ProjectSyncContainer(taskContainer, projectKey, false).execute();
+ }
+ new ProjectScanContainer(taskContainer, props).execute();
+ }
+
+ @CheckForNull
+ private static String getProjectKeyWithBranch(AnalysisProperties props) {
+ String projectKey = props.property(CoreProperties.PROJECT_KEY_PROPERTY);
+ if (projectKey != null && props.property(CoreProperties.PROJECT_BRANCH_PROPERTY) != null) {
+ projectKey = projectKey + ":" + props.property(CoreProperties.PROJECT_BRANCH_PROPERTY);
+ }
+ return projectKey;
+ }
+
+ private boolean isIssuesMode(AnalysisProperties props) {
+ DefaultAnalysisMode mode = new DefaultAnalysisMode(taskContainer.getComponentByType(GlobalProperties.class), props);
+ return mode.isIssues();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskContainer.java
new file mode 100644
index 00000000000..eee91f63055
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskContainer.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.task;
+
+import java.util.Map;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.task.Task;
+import org.sonar.api.task.TaskDefinition;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.bootstrap.ExtensionInstaller;
+import org.sonar.batch.bootstrap.ExtensionMatcher;
+import org.sonar.batch.bootstrap.ExtensionUtils;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.core.platform.ComponentContainer;
+
+public class TaskContainer extends ComponentContainer {
+
+ private final Map<String, String> taskProperties;
+ private final Object[] components;
+
+ public TaskContainer(ComponentContainer parent, Map<String, String> taskProperties, Object... components) {
+ super(parent);
+ this.taskProperties = taskProperties;
+ this.components = components;
+ }
+
+ @Override
+ protected void doBeforeStart() {
+ addTaskExtensions();
+ addCoreComponents();
+ for (Object component : components) {
+ add(component);
+ }
+ }
+
+ private void addCoreComponents() {
+ add(new TaskProperties(taskProperties, getParent().getComponentByType(GlobalProperties.class).property(CoreProperties.ENCRYPTION_SECRET_KEY_PATH)));
+ add(EmailSettings.class);
+ }
+
+ private void addTaskExtensions() {
+ getComponentByType(ExtensionInstaller.class).install(this, new TaskExtensionFilter());
+ }
+
+ static class TaskExtensionFilter implements ExtensionMatcher {
+ @Override
+ public boolean accept(Object extension) {
+ return ExtensionUtils.isBatchSide(extension)
+ && ExtensionUtils.isInstantiationStrategy(extension, InstantiationStrategy.PER_TASK);
+ }
+ }
+
+ @Override
+ public void doAfterStart() {
+ // default value is declared in CorePlugin
+ String taskKey = StringUtils.defaultIfEmpty(taskProperties.get(CoreProperties.TASK), CoreProperties.SCAN_TASK);
+ // Release memory
+ taskProperties.clear();
+
+ TaskDefinition def = getComponentByType(Tasks.class).definition(taskKey);
+ if (def == null) {
+ throw MessageException.of("Task '" + taskKey + "' does not exist. Please use '" + ListTask.KEY + "' task to see all available tasks.");
+ }
+ Task task = getComponentByType(def.taskClass());
+ if (task != null) {
+ task.execute();
+ } else {
+ throw new IllegalStateException("Task " + taskKey + " is badly defined");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskProperties.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskProperties.java
new file mode 100644
index 00000000000..b8470ce4d00
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/TaskProperties.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.task;
+
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.batch.bootstrap.UserProperties;
+
+/**
+ * Batch properties that are specific to a task (for example
+ * coming from sonar-project.properties).
+ */
+public class TaskProperties extends UserProperties {
+
+ public TaskProperties(Map<String, String> properties, @Nullable String pathToSecretKey) {
+ super(properties, pathToSecretKey);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/task/Tasks.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/Tasks.java
new file mode 100644
index 00000000000..ee68b62b1db
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/Tasks.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.task;
+
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.Maps;
+import java.util.Collection;
+import java.util.Map;
+import java.util.SortedMap;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.task.Task;
+import org.sonar.api.task.TaskDefinition;
+
+@BatchSide
+@InstantiationStrategy(InstantiationStrategy.PER_TASK)
+public class Tasks {
+
+ private final SortedMap<String, TaskDefinition> byKey;
+
+ public Tasks(TaskDefinition[] definitions) {
+ SortedMap<String, TaskDefinition> map = Maps.newTreeMap();
+ for (TaskDefinition definition : definitions) {
+ if (map.containsKey(definition.key())) {
+ throw new IllegalStateException("Task '" + definition.key() + "' is declared twice");
+ }
+ map.put(definition.key(), definition);
+ }
+ this.byKey = ImmutableSortedMap.copyOf(map);
+ }
+
+ public TaskDefinition definition(String taskKey) {
+ return byKey.get(taskKey);
+ }
+
+ public Collection<TaskDefinition> definitions() {
+ return byKey.values();
+ }
+
+ /**
+ * Perform validation of task definitions
+ */
+ public void start() {
+ checkDuplicatedClasses();
+ }
+
+ private void checkDuplicatedClasses() {
+ Map<Class<? extends Task>, TaskDefinition> byClass = Maps.newHashMap();
+ for (TaskDefinition def : definitions()) {
+ TaskDefinition other = byClass.get(def.taskClass());
+ if (other == null) {
+ byClass.put(def.taskClass(), def);
+ } else {
+ throw new IllegalStateException("Task '" + def.taskClass().getName() + "' is defined twice: first by '" + other.key() + "' and then by '" + def.key() + "'");
+ }
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/task/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/package-info.java
new file mode 100644
index 00000000000..5787c1e27c2
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/task/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.task;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultCoverageBlock.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultCoverageBlock.java
new file mode 100644
index 00000000000..52c3ca7a76f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultCoverageBlock.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.test;
+
+import java.util.List;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.test.CoverageBlock;
+import org.sonar.api.test.TestCase;
+import org.sonar.api.test.Testable;
+
+public class DefaultCoverageBlock implements CoverageBlock {
+
+ private final TestCase testCase;
+ private final DefaultInputFile testable;
+ private final List<Integer> lines;
+
+ public DefaultCoverageBlock(TestCase testCase, DefaultInputFile testable, List<Integer> lines) {
+ this.testCase = testCase;
+ this.testable = testable;
+ this.lines = lines;
+ }
+
+ @Override
+ public TestCase testCase() {
+ return testCase;
+ }
+
+ @Override
+ public Testable testable() {
+ return new DefaultTestable(testable);
+ }
+
+ @Override
+ public List<Integer> lines() {
+ return lines;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestCase.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestCase.java
new file mode 100644
index 00000000000..652d11a0ddb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestCase.java
@@ -0,0 +1,163 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.test;
+
+import com.google.common.base.Preconditions;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.test.CoverageBlock;
+import org.sonar.api.test.MutableTestCase;
+import org.sonar.api.test.TestPlan;
+import org.sonar.api.test.Testable;
+import org.sonar.api.test.exception.CoverageAlreadyExistsException;
+import org.sonar.api.test.exception.IllegalDurationException;
+
+public class DefaultTestCase implements MutableTestCase {
+
+ private final DefaultTestPlan testPlan;
+ private String type;
+ private Long durationInMs;
+ private Status status;
+ private String name;
+ private String message;
+ private String stackTrace;
+ private Map<DefaultInputFile, CoverageBlock> coverageBlocksByTestedFile = new LinkedHashMap<>();
+
+ public DefaultTestCase(DefaultTestPlan testPlan) {
+ this.testPlan = testPlan;
+ }
+
+ @Override
+ public String type() {
+ return type;
+ }
+
+ @Override
+ public MutableTestCase setType(@Nullable String s) {
+ this.type = s;
+ return this;
+ }
+
+ @Override
+ public Long durationInMs() {
+ return durationInMs;
+ }
+
+ @Override
+ public MutableTestCase setDurationInMs(@Nullable Long l) {
+ if (l != null && l < 0) {
+ throw new IllegalDurationException("Test duration must be positive (got: " + l + ")");
+ }
+ this.durationInMs = l;
+ return this;
+ }
+
+ @Override
+ public Status status() {
+ return status;
+ }
+
+ @Override
+ public MutableTestCase setStatus(@Nullable Status s) {
+ this.status = s;
+ return this;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ public MutableTestCase setName(String s) {
+ this.name = s;
+ return this;
+ }
+
+ @Override
+ public String message() {
+ return message;
+ }
+
+ @Override
+ public MutableTestCase setMessage(String s) {
+ this.message = s;
+ return this;
+ }
+
+ @Override
+ public String stackTrace() {
+ return stackTrace;
+ }
+
+ @Override
+ public MutableTestCase setStackTrace(String s) {
+ this.stackTrace = s;
+ return this;
+ }
+
+ @Override
+ public MutableTestCase setCoverageBlock(Testable testable, List<Integer> lines) {
+ DefaultInputFile coveredFile = ((DefaultTestable) testable).inputFile();
+ return setCoverageBlock(coveredFile, lines);
+ }
+
+ @Override
+ public MutableTestCase setCoverageBlock(InputFile mainFile, List<Integer> lines) {
+ Preconditions.checkArgument(mainFile.type() == Type.MAIN, "Test file can only cover a main file");
+ DefaultInputFile coveredFile = (DefaultInputFile) mainFile;
+ if (coverageBlocksByTestedFile.containsKey(coveredFile)) {
+ throw new CoverageAlreadyExistsException("The link between " + name() + " and " + coveredFile.key() + " already exists");
+ }
+ coverageBlocksByTestedFile.put(coveredFile, new DefaultCoverageBlock(this, coveredFile, lines));
+ return this;
+ }
+
+ @Override
+ public TestPlan testPlan() {
+ return testPlan;
+ }
+
+ @Override
+ public boolean doesCover() {
+ return !coverageBlocksByTestedFile.isEmpty();
+ }
+
+ @Override
+ public int countCoveredLines() {
+ throw new UnsupportedOperationException("Not supported since SQ 5.2");
+ }
+
+ @Override
+ public Iterable<CoverageBlock> coverageBlocks() {
+ return coverageBlocksByTestedFile.values();
+ }
+
+ @Override
+ public CoverageBlock coverageBlock(final Testable testable) {
+ DefaultInputFile coveredFile = ((DefaultTestable) testable).inputFile();
+ return coverageBlocksByTestedFile.get(coveredFile);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestPlan.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestPlan.java
new file mode 100644
index 00000000000..85b26082424
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestPlan.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.test;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import org.sonar.api.test.MutableTestCase;
+import org.sonar.api.test.MutableTestPlan;
+
+public class DefaultTestPlan implements MutableTestPlan {
+ private List<MutableTestCase> testCases = new ArrayList<>();
+
+ @Override
+ @CheckForNull
+ public Iterable<MutableTestCase> testCasesByName(String name) {
+ List<MutableTestCase> result = Lists.newArrayList();
+ for (MutableTestCase testCase : testCases()) {
+ if (name.equals(testCase.name())) {
+ result.add(testCase);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public MutableTestCase addTestCase(String name) {
+ DefaultTestCase testCase = new DefaultTestCase(this);
+ testCase.setName(name);
+ testCases.add(testCase);
+ return testCase;
+ }
+
+ @Override
+ public Iterable<MutableTestCase> testCases() {
+ return testCases;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestable.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestable.java
new file mode 100644
index 00000000000..c672253920e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/DefaultTestable.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.test.CoverageBlock;
+import org.sonar.api.test.MutableTestable;
+import org.sonar.api.test.TestCase;
+
+public class DefaultTestable implements MutableTestable {
+
+ private final DefaultInputFile inputFile;
+
+ public DefaultTestable(DefaultInputFile inputFile) {
+ this.inputFile = inputFile;
+ }
+
+ public DefaultInputFile inputFile() {
+ return inputFile;
+ }
+
+ @Override
+ public List<TestCase> testCases() {
+ throw unsupported();
+ }
+
+ @Override
+ public TestCase testCaseByName(final String name) {
+ throw unsupported();
+ }
+
+ @Override
+ public int countTestCasesOfLine(Integer line) {
+ throw unsupported();
+ }
+
+ @Override
+ public Map<Integer, Integer> testCasesByLines() {
+ throw unsupported();
+ }
+
+ @Override
+ public List<TestCase> testCasesOfLine(int line) {
+ throw unsupported();
+ }
+
+ @Override
+ public SortedSet<Integer> testedLines() {
+ throw unsupported();
+ }
+
+ @Override
+ public CoverageBlock coverageBlock(final TestCase testCase) {
+ throw unsupported();
+ }
+
+ @Override
+ public Iterable<CoverageBlock> coverageBlocks() {
+ throw unsupported();
+ }
+
+ private static UnsupportedOperationException unsupported() {
+ return new UnsupportedOperationException("No more available since SQ 5.2");
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestPlanBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestPlanBuilder.java
new file mode 100644
index 00000000000..b3bab1ee6f5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestPlanBuilder.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.test;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.test.MutableTestPlan;
+import org.sonar.batch.deprecated.perspectives.PerspectiveBuilder;
+import org.sonar.batch.index.BatchComponent;
+
+public class TestPlanBuilder extends PerspectiveBuilder<MutableTestPlan> {
+
+ private Map<InputFile, DefaultTestPlan> testPlanByFile = new HashMap<>();
+
+ public TestPlanBuilder() {
+ super(MutableTestPlan.class);
+ }
+
+ @CheckForNull
+ @Override
+ public MutableTestPlan loadPerspective(Class<MutableTestPlan> perspectiveClass, BatchComponent component) {
+ if (component.isFile()) {
+ InputFile inputFile = (InputFile) component.inputComponent();
+ if (inputFile.type() == Type.TEST) {
+ if (!testPlanByFile.containsKey(inputFile)) {
+ testPlanByFile.put(inputFile, new DefaultTestPlan());
+ }
+ return testPlanByFile.get(inputFile);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestableBuilder.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestableBuilder.java
new file mode 100644
index 00000000000..261b0b15a1d
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/TestableBuilder.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.test;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.test.MutableTestable;
+import org.sonar.batch.deprecated.perspectives.PerspectiveBuilder;
+import org.sonar.batch.index.BatchComponent;
+
+public class TestableBuilder extends PerspectiveBuilder<MutableTestable> {
+
+ public TestableBuilder() {
+ super(MutableTestable.class);
+ }
+
+ @CheckForNull
+ @Override
+ public MutableTestable loadPerspective(Class<MutableTestable> perspectiveClass, BatchComponent component) {
+ if (component.isFile()) {
+ InputFile inputFile = (InputFile) component.inputComponent();
+ if (inputFile.type() == Type.MAIN) {
+ return new DefaultTestable((DefaultInputFile) inputFile);
+ }
+ }
+ return null;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/test/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/package-info.java
new file mode 100644
index 00000000000..cb0a2f6c3e7
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/test/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public 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.batch.test;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/util/BatchUtils.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/util/BatchUtils.java
new file mode 100644
index 00000000000..81eed874378
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/util/BatchUtils.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.util;
+
+import com.google.common.base.Strings;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BatchUtils {
+ private static final Logger LOG = LoggerFactory.getLogger(BatchUtils.class);
+
+ private BatchUtils() {
+ }
+
+ /**
+ * Clean provided string to remove chars that are not valid as file name.
+ * @param projectKey e.g. my:file
+ */
+ public static String cleanKeyForFilename(String projectKey) {
+ String cleanKey = StringUtils.deleteWhitespace(projectKey);
+ return StringUtils.replace(cleanKey, ":", "_");
+ }
+
+ public static String encodeForUrl(@Nullable String url) {
+ try {
+ return URLEncoder.encode(Strings.nullToEmpty(url), "UTF-8");
+
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException("Encoding not supported", e);
+ }
+ }
+
+ public static String describe(Object o) {
+ try {
+ if (o.getClass().getMethod("toString").getDeclaringClass() != Object.class) {
+ return o.toString();
+ }
+ } catch (Exception e) {
+ // fallback
+ }
+
+ return o.getClass().getName();
+ }
+
+ @CheckForNull
+ public static String getServerVersion() {
+ InputStream is = BatchUtils.class.getResourceAsStream("/sq-version.txt");
+ if (is == null) {
+ LOG.warn("Failed to get SQ version");
+ return null;
+ }
+ try (BufferedReader br = IOUtils.toBufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ return br.readLine();
+ } catch (IOException e) {
+ LOG.warn("Failed to get SQ version", e);
+ return null;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/util/ProgressReport.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/util/ProgressReport.java
new file mode 100644
index 00000000000..f2ea0406a01
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/util/ProgressReport.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.util;
+
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+public class ProgressReport implements Runnable {
+
+ private static final Logger LOG = Loggers.get(ProgressReport.class);
+ private final long period;
+ private String message = "";
+ private final Thread thread;
+ private String stopMessage = "";
+
+ public ProgressReport(String threadName, long period) {
+ this.period = period;
+ thread = new Thread(this);
+ thread.setName(threadName);
+ thread.setDaemon(true);
+ }
+
+ @Override
+ public void run() {
+ while (!Thread.interrupted()) {
+ try {
+ Thread.sleep(period);
+ log(message);
+ } catch (InterruptedException e) {
+ break;
+ }
+ }
+ log(stopMessage);
+ }
+
+ public void start(String startMessage) {
+ log(startMessage);
+ thread.start();
+ }
+
+ public void message(String message) {
+ this.message = message;
+ }
+
+ public void stop(String stopMessage) {
+ this.stopMessage = stopMessage;
+ thread.interrupt();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+
+ private static void log(String message) {
+ synchronized (LOG) {
+ LOG.info(message);
+ LOG.notifyAll();
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/util/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/util/package-info.java
new file mode 100644
index 00000000000..42cf7aa9450
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/util/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.batch.util;
+
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml b/sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml
new file mode 100644
index 00000000000..99f956e4b4c
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+
+ <!--
+
+ This file is loaded by bootstrappers like Ant Task and Java Runner.
+
+ Reasons to NOT move this configuration to bootstrappers:
+ - same lifecycle as sonar -> loggers are always up-to-date. No need to think about ascending/descending compatibility.
+ - parameters can be added without releasing new versions of bootstrappers
+ - XML format is up-to-date toward the version of Logback.
+
+ -->
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>${FORMAT}</pattern>
+ </encoder>
+ </appender>
+
+ <!-- BeanUtils generate to many DEBUG logs when sonar.verbose is set -->
+ <logger name="org.apache.commons.beanutils.converters">
+ <level value="WARN"/>
+ </logger>
+
+ <!-- sonar.showSql -->
+ <!-- see also org.sonar.db.MyBatis#configureLogback() -->
+ <logger name="org.mybatis">
+ <level value="${SQL_LOGGER_LEVEL:-WARN}"/>
+ </logger>
+ <logger name="org.apache.ibatis">
+ <level value="${SQL_LOGGER_LEVEL:-WARN}"/>
+ </logger>
+ <logger name="java.sql">
+ <level value="${SQL_LOGGER_LEVEL:-WARN}"/>
+ </logger>
+ <logger name="java.sql.ResultSet">
+ <level value="WARN"/>
+ </logger>
+ <logger name="PERSISTIT">
+ <level value="WARN"/>
+ </logger>
+
+ <root>
+ <!-- sonar.verbose -->
+ <level value="${ROOT_LOGGER_LEVEL}"/>
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration>
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/logback.xml b/sonar-scanner-engine/src/main/resources/org/sonar/batch/logback.xml
new file mode 100644
index 00000000000..198cd9c1bc6
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/logback.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+
+ <!--
+
+ This file is deprecated. It's replaced by org/sonar/batch/bootstrapper/logback.xml.
+ It can't be deleted as long as Ant Task and Java Runner do not use org.sonar.batch.bootstrapper.LoggingConfiguration.
+
+ -->
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} %-5level %20.20logger{20} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <!-- BeanUtils generate to many DEBUG logs when sonar.verbose is set -->
+ <logger name="org.apache.commons.beanutils.converters">
+ <level value="WARN"/>
+ </logger>
+
+ <!-- sonar.showSql -->
+ <!-- see also org.sonar.db.MyBatis#configureLogback() -->
+ <logger name="org.apache.ibatis">
+ <level value="WARN"/>
+ </logger>
+ <logger name="org.mybatis">
+ <level value="WARN"/>
+ </logger>
+ <logger name="java.sql">
+ <level value="WARN"/>
+ </logger>
+ <logger name="java.sql.ResultSet">
+ <level value="WARN"/>
+ </logger>
+ <logger name="PERSISTIT">
+ <level value="WARN"/>
+ </logger>
+
+
+ <root>
+ <!-- sonar.verbose -->
+ <level value="${ROOT_LOGGER_LEVEL}"/>
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration>
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport.ftl b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport.ftl
new file mode 100644
index 00000000000..c41b8067222
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport.ftl
@@ -0,0 +1,462 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Issues report of ${report.getTitle()?html}</title>
+ <link href="issuesreport_files/sonar.css" media="all" rel="stylesheet" type="text/css">
+ <link rel="shortcut icon" type="image/x-icon" href="issuesreport_files/favicon.ico">
+ <script type="text/javascript" src="issuesreport_files/jquery.min.js"></script>
+ <script type="text/javascript">
+ var issuesPerResource = [
+ <#list report.getResourceReports() as resourceReport>
+ [
+ <#assign issues=resourceReport.getIssues()>
+ <#list issues as issue>
+ <#if complete || issue.isNew()>
+ {'k': '${issue.key()}', 'r': 'R${issue.getRuleKey()}', 'l': ${(issue.startLine()!0)?c}, 'new': ${issue.isNew()?string}, 's': '${issue.severity()?lower_case}'}<#if issue_has_next>,</#if>
+ </#if>
+ </#list>
+ ]
+ <#if resourceReport_has_next>,</#if>
+ </#list>
+ ];
+ var nbResources = ${report.getResourcesWithReport()?size?c};
+ var separators = new Array();
+
+ function showLine(fileIndex, lineId) {
+ var elt = $('#' + fileIndex + 'L' + lineId);
+ if (elt != null) {
+ elt.show();
+ }
+ elt = $('#' + fileIndex + 'LV' + lineId);
+ if (elt != null) {
+ elt.show();
+ }
+ }
+
+ /* lineIds must be sorted */
+ function showLines(fileIndex, lineIds) {
+ var lastSeparatorId = 9999999;
+ for (var lineIndex = 0; lineIndex < lineIds.length; lineIndex++) {
+ var lineId = lineIds[lineIndex];
+ if (lineId > 0) {
+ if (lineId > lastSeparatorId) {
+ var separator = $('#' + fileIndex + 'S' + lastSeparatorId);
+ if (separator != null) {
+ separator.addClass('visible');
+ separators.push(separator);
+ }
+ }
+
+ for (var i = -2; i < 3; ++i) {
+ showLine(fileIndex, lineId + i);
+ }
+ lastSeparatorId = lineId + 2;
+ }
+ }
+ }
+ function hideAll() {
+ $('tr.row').hide();
+ $('div.issue').hide();
+ for (var separatorIndex = 0; separatorIndex < separators.length; separatorIndex++) {
+ separators[separatorIndex].removeClass('visible');
+ }
+ separators.length = 0;
+ $('.sources td.ko').removeClass('ko');
+ }
+
+ function showIssues(fileIndex, issues) {
+ $.each(issues, function(index, issue) {
+ $('#' + issue['k']).show();
+ $('#' + fileIndex + 'L' + issue['l'] + ' td.line').addClass('ko');
+ });
+ var showResource = issues.length > 0;
+ if (showResource) {
+ $('#resource-' + fileIndex).show();
+ } else {
+ $('#resource-' + fileIndex).hide();
+ }
+ }
+
+
+ function refreshFilters(updateSelect) {
+ <#if complete>
+ var onlyNewIssues = $('#new_filter').is(':checked');
+ <#else>
+ var onlyNewIssues = true;
+ </#if>
+
+ if (updateSelect) {
+ populateSelectFilter(onlyNewIssues);
+ }
+ var ruleFilter = $('#rule_filter').val();
+
+ hideAll();
+ if (onlyNewIssues) {
+ $('.all').addClass('all-masked');
+ } else {
+ $('.all').removeClass('all-masked');
+ }
+ for (var resourceIndex = 0; resourceIndex < nbResources; resourceIndex++) {
+ var filteredIssues = $.grep(issuesPerResource[resourceIndex], function(v) {
+ return (!onlyNewIssues || v['new']) && (ruleFilter == '' || v['r'] == ruleFilter || v['s'] == ruleFilter);
+ }
+ );
+
+ var linesToDisplay = $.map(filteredIssues, function(v, i) {
+ return v['l'];
+ });
+
+ linesToDisplay.sort();// the showLines() requires sorted ids
+ showLines(resourceIndex, linesToDisplay);
+ showIssues(resourceIndex, filteredIssues);
+ }
+ }
+
+
+ var severityFilter = [
+ <#assign severities = report.getSummary().getTotalBySeverity()>
+ <#list severities?keys as severity>
+ { "key": "${severity?lower_case}",
+ "label": "${severity?lower_case?cap_first}",
+ "total": ${severities[severity].getCountInCurrentAnalysis()?c},
+ "newtotal": ${severities[severity].getNewIssuesCount()?c}
+ }<#if severity_has_next>,</#if>
+ </#list>
+ ];
+
+ var ruleFilter = [
+ <#assign rules = report.getSummary().getTotalByRuleKey()>
+ <#list rules?keys as ruleKey>
+ { "key": "${ruleKey}",
+ "label": "${ruleNameProvider.nameForJS(ruleKey)}",
+ "total": ${rules[ruleKey].getCountInCurrentAnalysis()?c},
+ "newtotal": ${rules[ruleKey].getNewIssuesCount()?c}
+ }<#if ruleKey_has_next>,</#if>
+ </#list>
+ ].sort(function(a, b) {
+ var x = a.label; var y = b.label;
+ return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+ });
+
+ function populateSelectFilter(onlyNewIssues) {
+ var ruleFilterSelect = $('#rule_filter');
+ ruleFilterSelect.empty().append(function() {
+ var output = '';
+ output += '<option value="" selected>Filter by:</option>';
+ output += '<optgroup label="Severity">';
+ $.each(severityFilter, function(key, value) {
+ if ((!onlyNewIssues && value.total > 0) || value.newtotal > 0) {
+ output += '<option value="' + value.key + '">' + value.label + ' (' + (onlyNewIssues ? value.newtotal : value.total) + ')</option>';
+ }
+ });
+ output += '<optgroup label="Rule">';
+ $.each(ruleFilter, function(key, value) {
+ if ((!onlyNewIssues && value.total > 0) || value.newtotal > 0) {
+ output += '<option value="R' + value.key + '">' + value.label + ' (' + (onlyNewIssues ? value.newtotal : value.total) + ')</option>';
+ }
+ });
+ return output;
+ });
+ }
+ </script>
+</head>
+<body>
+<div id="reportHeader">
+ <div id="logo"><img src="issuesreport_files/sonarqube-24x100.png" alt="SonarQube"/></div>
+ <div class="title">Issues Report</div>
+ <div class="subtitle">${report.getTitle()?html} - ${report.getDate()?datetime}</div>
+</div>
+
+<#if report.isNoFile()>
+<div id="content">
+ <div class="banner">No file analyzed</div>
+</div>
+<#else>
+<div id="content">
+
+ <#if !complete>
+ <div class="banner">Light report: only new issues are displayed</div>
+ </#if>
+
+ <div id="summary">
+ <table width="100%">
+ <tbody>
+ <tr>
+ <#if complete>
+ <#assign size = '33'>
+ <#else>
+ <#assign size = '50'>
+ </#if>
+ <td align="center" width="${size}%">
+ <h3>New issues</h3>
+ <#if report.getSummary().getTotal().getNewIssuesCount() gt 0>
+ <span class="big worst">${report.getSummary().getTotal().getNewIssuesCount()?c}</span>
+ <#else>
+ <span class="big">0</span>
+ </#if>
+ </td>
+ <td align="center" width="${size}%">
+ <h3>Resolved issues</h3>
+ <#if report.getSummary().getTotal().getResolvedIssuesCount() gt 0>
+ <span class="big better">${report.getSummary().getTotal().getResolvedIssuesCount()?c}</span>
+ <#else>
+ <span class="big">0</span>
+ </#if>
+ </td>
+ <#if complete>
+ <td align="center" width="${size}%" class="all">
+ <h3>Issues</h3>
+ <span class="big">${report.getSummary().getTotal().getCountInCurrentAnalysis()?c}</span>
+ </td>
+ </#if>
+ </tr>
+ </tbody>
+ </table>
+ <#if complete>
+ <br/>
+ <table width="100%" class="data">
+ <thead>
+ <tr class="total">
+ <th colspan="2" align="left">
+ Issues per Rule
+ </th>
+ <th align="right" width="1%" nowrap>New issues</th>
+ <th align="right" width="1%" nowrap>Resolved issues</th>
+ <th align="right" width="1%" nowrap class="all">Issues</th>
+ </tr>
+ </thead>
+ <tbody>
+ <#list report.getSummary().getRuleReports() as ruleReport>
+ <#if complete || (ruleReport.getTotal().getNewIssuesCount() > 0)>
+ <#if ruleReport.getTotal().getNewIssuesCount() = 0>
+ <#assign trCss = 'all'>
+ <#else>
+ <#assign trCss = ''>
+ </#if>
+ <tr class="hoverable ${trCss}">
+ <td width="20">
+ <i class="icon-severity-${ruleReport.getSeverity()?lower_case}"></i>
+ </td>
+ <td align="left">
+ ${ruleNameProvider.nameForHTML(ruleReport.getRule())}
+ </td>
+ <td align="right">
+ <#if ruleReport.getTotal().getNewIssuesCount() gt 0>
+ <span class="worst">${ruleReport.getTotal().getNewIssuesCount()?c}</span>
+ <#else>
+ <span>0</span>
+ </#if>
+ </td>
+ <td align="right">
+ <#if ruleReport.getTotal().getResolvedIssuesCount() gt 0>
+ <span class="better">${ruleReport.getTotal().getResolvedIssuesCount()?c}</span>
+ <#else>
+ <span>0</span>
+ </#if>
+ </td>
+ <td align="right" class="all">
+ ${ruleReport.getTotal().getCountInCurrentAnalysis()?c}
+ </td>
+ </tr>
+ </#if>
+ </#list>
+ </tbody>
+ </table>
+ </#if>
+ </div>
+
+ <br/>
+
+ <div class="banner">
+ <#if complete>
+ <input type="checkbox" id="new_filter" onclick="refreshFilters(true)" checked="checked" /> <label for="new_filter">Only NEW
+ issues</label>
+ &nbsp;&nbsp;&nbsp;&nbsp;
+ </#if>
+
+ <select id="rule_filter" onchange="refreshFilters(false)">
+ </select>
+ </div>
+
+ <div id="summary-per-file">
+ <#list report.getResourceReports() as resourceReport>
+ <#if complete || (resourceReport.getTotal().getNewIssuesCount() > 0)>
+ <#assign issueId=0>
+ <#if resourceReport.getTotal().getNewIssuesCount() = 0>
+ <#assign tableCss = 'all'>
+ <#else>
+ <#assign tableCss = ''>
+ </#if>
+ <table width="100%" class="data ${tableCss}" id="resource-${resourceReport_index?c}">
+ <thead>
+ <tr class="total">
+ <th align="left" colspan="2" nowrap>
+ <div class="file_title">
+ <img src="issuesreport_files/${resourceReport.getType()}.png" title="Resource icon"/>
+ <a href="#" onclick="$('.resource-details-${resourceReport_index?c}').toggleClass('masked'); return false;" style="color: black">${resourceReport.getName()}</a>
+ </div>
+ </th>
+ <th align="right" width="1%" nowrap class="resource-details-${resourceReport_index?c}">
+ <#if resourceReport.getTotal().getNewIssuesCount() gt 0>
+ <span class="worst" id="new-total">${resourceReport.getTotal().getNewIssuesCount()?c}</span>
+ <#else>
+ <span id="new-total">0</span>
+ </#if>
+ <br/>New issues
+ </th>
+ <#if complete>
+ <th align="right" width="1%" nowrap class="resource-details-${resourceReport_index?c}">
+ <#if resourceReport.getTotal().getResolvedIssuesCount() gt 0>
+ <span class="better" id="resolved-total">${resourceReport.getTotal().getResolvedIssuesCount()?c}</span>
+ <#else>
+ <span id="resolved-total">0</span>
+ </#if>
+ <br/>Resolved issues
+ </th>
+ <th align="right" width="1%" nowrap class="resource-details-${resourceReport_index?c} all">
+ <span id="current-total">${resourceReport.getTotal().getCountInCurrentAnalysis()?c}</span><br/>Issues
+ </th>
+ </#if>
+ </tr>
+ </thead>
+ <tbody class="resource-details-${resourceReport_index?c}">
+ <#if complete>
+ <#list resourceReport.getRuleReports() as ruleReport>
+ <tr class="hoverable all">
+ <td width="20">
+ <i class="icon-severity-${ruleReport.getSeverity()?lower_case}"></i>
+ </td>
+ <td align="left">
+ ${ruleNameProvider.nameForHTML(ruleReport.getRule())}
+ </td>
+ <td align="right">
+ <#if ruleReport.getTotal().getNewIssuesCount() gt 0>
+ <span class="worst">${ruleReport.getTotal().getNewIssuesCount()?c}</span>
+ <#else>
+ <span>0</span>
+ </#if>
+ </td>
+ <#if complete>
+ <td align="right">
+ <#if ruleReport.getTotal().getResolvedIssuesCount() gt 0>
+ <span class="better">${ruleReport.getTotal().getResolvedIssuesCount()?c}</span>
+ <#else>
+ <span>0</span>
+ </#if>
+ </td>
+ <td align="right" class="all">
+ ${ruleReport.getTotal().getCountInCurrentAnalysis()?c}
+ </td>
+ </#if>
+ </tr>
+ </#list>
+ </#if>
+ <#if complete>
+ <#assign colspan = '5'>
+ <#else>
+ <#assign colspan = '3'>
+ </#if>
+ <#assign issues=resourceReport.getIssuesAtLine(0, complete)>
+ <#if issues?has_content>
+ <tr class="globalIssues">
+ <td colspan="${colspan}">
+ <#list issues as issue>
+ <div class="issue" id="${issue.key()}">
+ <div class="vtitle">
+ <i class="icon-severity-${issue.severity()?lower_case}"></i>
+ <#if issue.getMessage()?has_content>
+ <span class="rulename">${issue.getMessage()?html}</span>
+ <#else>
+ <span class="rulename">${ruleNameProvider.nameForHTML(issue.getRuleKey())}</span>
+ </#if>
+ &nbsp;
+ <img src="issuesreport_files/sep12.png">&nbsp;
+
+ <span class="issue_date">
+ <#if issue.isNew()>
+ NEW
+ <#else>
+ ${issue.creationDate()?date}
+ </#if>
+ </span>
+ </div>
+ <div class="discussionComment">
+ ${ruleNameProvider.nameForHTML(issue.getRuleKey())}
+ </div>
+ </div>
+ <#assign issueId = issueId + 1>
+ </#list>
+ </td>
+ </tr>
+ </#if>
+ <tr>
+ <td colspan="${colspan}">
+ <table class="sources" border="0" cellpadding="0" cellspacing="0">
+ <#list sourceProvider.getEscapedSource(resourceReport.getResourceNode()) as line>
+ <#assign lineIndex=line_index+1>
+ <#if resourceReport.isDisplayableLine(lineIndex, complete)>
+ <tr id="${resourceReport_index?c}L${lineIndex?c}" class="row">
+ <td class="lid ">${lineIndex?c}</td>
+ <td class="line ">
+ <pre>${line}</pre>
+ </td>
+ </tr>
+ <tr id="${resourceReport_index}S${lineIndex?c}" class="blockSep">
+ <td colspan="2"></td>
+ </tr>
+ <#assign issues=resourceReport.getIssuesAtLine(lineIndex, complete)>
+ <#if issues?has_content>
+ <tr id="${resourceReport_index?c}LV${lineIndex?c}" class="row">
+ <td class="lid"></td>
+ <td class="issues">
+ <#list issues as issue>
+ <div class="issue" id="${issue.key()}">
+ <div class="vtitle">
+ <i class="icon-severity-${issue.severity()?lower_case}"></i>
+ <#if issue.getMessage()?has_content>
+ <span class="rulename">${issue.getMessage()?html}</span>
+ <#else>
+ <span class="rulename">${ruleNameProvider.nameForHTML(issue.getRuleKey())}</span>
+ </#if>
+ &nbsp;
+ <img src="issuesreport_files/sep12.png">&nbsp;
+
+ <span class="issue_date">
+ <#if issue.isNew()>
+ NEW
+ <#else>
+ ${issue.creationDate()?date}
+ </#if>
+ </span>
+ &nbsp;
+
+ </div>
+ <div class="discussionComment">
+ ${ruleNameProvider.nameForHTML(issue.getRuleKey())}
+ </div>
+ </div>
+ <#assign issueId = issueId + 1>
+ </#list>
+ </td>
+ </tr>
+ </#if>
+ </#if>
+ </#list>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </#if>
+ </#list>
+ </div>
+</div>
+<script type="text/javascript">
+ $(function() {
+ refreshFilters(true);
+ });
+</script>
+</#if>
+</body>
+</html>
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/DIR.png b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/DIR.png
new file mode 100644
index 00000000000..b135ef92eec
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/DIR.png
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/FIL.png b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/FIL.png
new file mode 100644
index 00000000000..1664e25c8b5
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/FIL.png
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/PRJ.png b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/PRJ.png
new file mode 100644
index 00000000000..b32e51c5f42
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/PRJ.png
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/favicon.ico b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/favicon.ico
new file mode 100644
index 00000000000..c6d382d9823
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/favicon.ico
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/jquery.min.js b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/jquery.min.js
new file mode 100644
index 00000000000..53763631337
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/jquery.min.js
@@ -0,0 +1,6 @@
+/*! jQuery v1.10.1 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
+ //@ sourceMappingURL=jquery-1.10.1.min.map
+ */
+(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.1",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=lt(),k=lt(),E=lt(),S=!1,A=function(){return 0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=bt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+xt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return At(e.replace(z,"$1"),t,n,i)}function st(e){return K.test(e+"")}function lt(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function ut(e){return e[b]=!0,e}function ct(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function pt(e,t,n){e=e.split("|");var r,i=e.length,a=n?null:t;while(i--)(r=o.attrHandle[e[i]])&&r!==t||(o.attrHandle[e[i]]=a)}function ft(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:e[t]===!0?t.toLowerCase():null}function dt(e,t){return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function ht(e){return"input"===e.nodeName.toLowerCase()?e.defaultValue:t}function gt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function mt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function yt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function vt(e){return ut(function(t){return t=+t,ut(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.parentWindow;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.frameElement&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ct(function(e){return e.innerHTML="<a href='#'></a>",pt("type|href|height|width",dt,"#"===e.firstChild.getAttribute("href")),pt(B,ft,null==e.getAttribute("disabled")),e.className="i",!e.getAttribute("className")}),r.input=ct(function(e){return e.innerHTML="<input>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")}),pt("value",ht,r.attributes&&r.input),r.getElementsByTagName=ct(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ct(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ct(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=st(n.querySelectorAll))&&(ct(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ct(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=st(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ct(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=st(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},r.sortDetached=ct(function(e){return 1&e.compareDocumentPosition(n.createElement("div"))}),A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return gt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?gt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:ut,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=bt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?ut(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ut(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?ut(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ut(function(e){return function(t){return at(e,t).length>0}}),contains:ut(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:ut(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:vt(function(){return[0]}),last:vt(function(e,t){return[t-1]}),eq:vt(function(e,t,n){return[0>n?n+t:n]}),even:vt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:vt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:vt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:vt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=mt(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=yt(n);function bt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function xt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function wt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function Tt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function Ct(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function Nt(e,t,n,r,i,o){return r&&!r[b]&&(r=Nt(r)),i&&!i[b]&&(i=Nt(i,o)),ut(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||St(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:Ct(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=Ct(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=Ct(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function kt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=wt(function(e){return e===t},s,!0),p=wt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[wt(Tt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return Nt(l>1&&Tt(f),l>1&&xt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&kt(e.slice(l,r)),i>r&&kt(e=e.slice(r)),i>r&&xt(e))}f.push(n)}return Tt(f)}function Et(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=Ct(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?ut(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=bt(e)),n=t.length;while(n--)o=kt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Et(i,r))}return o};function St(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function At(e,t,n,i){var a,s,u,c,p,f=bt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&xt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}o.pseudos.nth=o.pseudos.eq;function jt(){}jt.prototype=o.filters=o.pseudos,o.setFilters=new jt,r.sortStable=b.split("").sort(A).join("")===b,p(),[0,0].sort(A),r.detectDuplicates=S,x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!l||i&&!u||(n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)
+}),n=s=l=u=r=o=null,t}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,r="boolean"==typeof t;return x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,a=0,s=x(this),l=t,u=e.match(T)||[];while(o=u[a++])l=r?l:!s.hasClass(o),s[l?"addClass":"removeClass"](o)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
+ u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:nn(this))?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sep12.png b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sep12.png
new file mode 100644
index 00000000000..bb10431c778
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sep12.png
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.css b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.css
new file mode 100644
index 00000000000..40e929cf077
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.css
@@ -0,0 +1,396 @@
+html {
+ color: #111;
+ background: #FFF;
+}
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}
+body {
+ font: 13px/1.231 arial, helvetica, clean, sans-serif;
+}
+#content {
+ padding: 5px;
+}
+#reportHeader {
+ background-color: #262626;
+ height: 40px;
+ color: #FFF;
+ padding: 5px 10px 0 10px;
+ vertical-align: top;
+ text-align: center;
+}
+#logo {
+ float: right;
+}
+#reportHeader .title {
+ font-size: 18px;
+}
+#reportHeader .subtitle {
+ font-size: 11px;
+}
+#rules {
+ margin-bottom: 25px;
+}
+#rules a, #rules a:visited {
+ color: #111;
+}
+#rules .ruleSubtitle, #rules .ruleSubtitle a, #rules .ruleSubtitle a:visited {
+ font-size: 93%;
+ color: #777;
+}
+.globalIssues {
+ width: 100%;
+ border: 0;
+ margin-left: 37px;
+}
+hr {
+ border: none;
+ border-top: 2px dashed #DDD;
+ color: #FFF;
+ height: 10px;
+}
+div.banner {
+ height: 26px;
+ line-height: 26px;
+ background-color: #EEEEEE;
+ border: 1px solid #DDDDDD;
+ color: #444;
+ font-size: 85%;
+ margin-bottom: 10px;
+ padding: 0 5px;
+}
+table.data > thead > tr > th {
+ font-size: 93%;
+ padding: 4px 7px 4px 3px;
+ font-weight: normal;
+}
+table.data > tfoot > tr > td {
+ font-size: 93%;
+ color: #777;
+ padding: 4px 0 4px 10px;
+}
+
+table.data > tbody > tr > td {
+ padding: 4px 7px 4px 3px;
+ vertical-align: text-top;
+}
+
+table.data td.small, table.data th.small {
+ padding: 0;
+ white-space: nowrap;
+}
+
+table.data th img, table.data td img {
+ vertical-align: text-bottom;
+}
+
+.data thead tr.total {
+ background-color: #ECECEC;
+ font-weight: normal;
+ border: 1px solid #DDD;
+}
+
+.data thead tr.total th {
+ font-weight: normal;
+}
+
+table.data > thead {
+ border-bottom: 1px solid #ddd;
+}
+
+table.data > tbody {
+ border-bottom: 1px solid #ddd;
+ border-right: 1px solid #ddd;
+ border-left: 1px solid #ddd;
+}
+
+table.data, table.spaced, .gwt-SourcePanel .sources {
+ width: 100%;
+}
+
+table.data>thead>tr>th {
+ font-size: 93%;
+ padding: 4px 7px 4px 3px;
+}
+
+table.data>tfoot>tr>td {
+ font-size: 93%;
+ color: #777;
+ padding: 4px 0 4px 10px;
+}
+
+table.data>tbody>tr>td {
+ padding: 4px 7px 4px 3px;
+ vertical-align: text-top;
+}
+
+.data thead tr.total {
+ background-color: #ECECEC;
+}
+
+.data thead tr.total th {
+ font-weight: bold;
+}
+
+.hoverable:hover {
+ background-color: #f7f7f7;
+}
+
+div, ul, li, h2, pre, form, input, td {
+ margin: 0;
+ padding: 0;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+img {
+ border: 0;
+}
+
+select, input, textarea {
+ font: 99% arial, helvetica, clean, sans-serif;
+}
+
+pre, code {
+ font-family: monospace;
+ line-height: 100%;
+}
+
+code {
+ font-size: 93%;
+}
+
+em {
+ font-weight: bold;
+}
+
+h1 {
+ color: #444;
+ font-size: 16px;
+}
+
+h2 {
+ color: #2B547D;
+ font-size: 16px;
+ font-weight: normal;
+}
+
+h3 {
+ font-size: 100%;
+ font-weight: bold;
+}
+
+h4 {
+ font-size: 85%;
+ color: #777;
+}
+td.sep {
+ width: 10px;
+}
+.sources {
+ width: 100%;
+ border-top: 1px solid #DDD;
+ border-bottom: 1px solid #DDD;
+ margin: 0 0 25px 0;
+}
+
+.sources td.lid {
+ background-color: #ECECEC;
+ border-right: 1px solid #DDD;
+ border-left: 1px solid #DDD;
+ text-align: right;
+ padding: 2px .5em 0 .5em;
+ vertical-align: top;
+ font-size: 85%;
+ color: #444;
+ min-width: 25px;
+}
+div.issue {
+ background-color: #FFF;
+ border: 1px solid #DDD;
+ margin: 7px;
+}
+
+div.vtitle {
+ background-color: #E4ECF3;
+ line-height: 2.2em;
+ height: 2.2em;
+ margin: 0;
+ padding: 0 0 0 10px;
+ text-shadow: 0 1px 0 #FFF;
+ color: #777;
+ vertical-align: middle;
+}
+
+.rulename {
+ color: #444;
+ font-weight: bold;
+}
+
+span.issue_date {
+ color: #777;
+ font-size: 90%;
+}
+
+.sources td.line {
+ width: 100%;
+ border-right: 1px solid #DDD;
+}
+
+.sources td.line pre {
+ font-size: 12px;
+ font-family: monospace;
+ margin-left: 1em;
+}
+
+.sources td.ko {
+ background-color: #FF9090;
+}
+
+.sources td.new_section {
+ border-top: 1px solid #DDD;
+ border-bottom: 1px solid #DDD;
+ height: 40px;
+}
+
+td.issues {
+ background-color: #FFF;
+ border-right: 1px solid #DDD;
+ margin: 0;
+}
+
+.sources pre {
+ font-family: Monospace;
+ margin: 0;
+ padding: 0 5px;
+ color: #333;
+ margin: 0;
+}
+
+.file_title {
+ line-height: 1.5em;
+ height: 1.5em;
+ font-size: 1.2em;
+ margin: 10px 0 5px 0;
+ vertical-align: middle;
+}
+
+.file_title img {
+ vertical-align: middle;
+}
+
+.discussionComment.first {
+ border-top: none;
+}
+
+div.discussionComment {
+ background-color: #F4F4F4;
+ border-top: 1px solid #DDD;
+ line-height: 1.5em;
+ margin: 0;
+ padding: 5px 10px;
+}
+
+.rule_desc pre {
+ margin: 10px 0;
+ font-family: "Courier New", Courier, monospace;;
+ border: 1px dashed #aaa;
+ font-size: 93%;
+}
+span.better {
+ color: green;
+}
+span.worst {
+ color: red;
+}
+tr.blockSep {
+ display: none;
+}
+tr.blockSep.visible {
+ height: 20px;
+ line-height: 20px;
+ margin: 1px 0;
+ display: table-row !important;
+ display: block; /* hack for IE */
+ border-top: 1px dashed #DDD;
+ border-bottom: 1px dashed #DDD;
+}
+
+.big {
+ font-size:24px;
+ font-weight: bold;
+}
+
+.masked {
+ display: none;
+}
+
+.all-masked {
+ display: none;
+}
+
+/**
+ Icons
+ */
+
+@font-face {
+ font-family: 'sonar';
+ src: url('sonar.eot');
+ src: url('sonar.eot?#iefix') format('embedded-opentype'), url('sonar.ttf') format('truetype'), url('sonar.woff') format('woff'), url('sonar.svg#sonar') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+[class^="icon-"],
+[class*=" icon-"] {
+ font-family: 'sonar';
+ speak: none;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ vertical-align: middle;
+ /* Better Font Rendering =========== */
+
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+/*
+ * Severity
+ */
+[class^="icon-severity-"],
+[class*=" icon-severity"] {
+ position: relative;
+ top: -1px;
+}
+.icon-severity-blocker:before,
+.icon-severity-4:before {
+ content: "\f000";
+ color: #d4333f;
+ font-size: 14px;
+}
+.icon-severity-critical:before,
+.icon-severity-3:before {
+ content: "\f001";
+ color: #d4333f;
+ font-size: 14px;
+}
+.icon-severity-major:before,
+.icon-severity-2:before {
+ content: "\f002";
+ color: #d4333f;
+ font-size: 14px;
+}
+.icon-severity-minor:before,
+.icon-severity-1:before {
+ content: "\f003";
+ color: #85bb43;
+ font-size: 14px;
+}
+.icon-severity-info:before,
+.icon-severity-0:before {
+ content: "\f004";
+ color: #85bb43;
+ font-size: 14px;
+}
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.eot b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.eot
new file mode 100755
index 00000000000..189f7b56818
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.eot
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.svg b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.svg
new file mode 100755
index 00000000000..36965167290
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Generated by IcoMoon</metadata>
+<defs>
+<font id="sonar" horiz-adv-x="1024">
+<font-face units-per-em="1024" ascent="960" descent="-64" />
+<missing-glyph horiz-adv-x="1024" />
+<glyph unicode="&#x20;" d="" horiz-adv-x="512" />
+<glyph unicode="&#xf000;" d="M438.857 865.524q119.429 0 220.286-58.857t159.714-159.714 58.857-220.286-58.857-220.286-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857zM512 152.953v108.571q0 8-5.143 13.429t-12.571 5.429h-109.714q-7.429 0-13.143-5.714t-5.714-13.143v-108.571q0-7.429 5.714-13.143t13.143-5.714h109.714q7.429 0 12.571 5.429t5.143 13.429zM510.857 349.524l10.286 354.857q0 6.857-5.714 10.286-5.714 4.571-13.714 4.571h-125.714q-8 0-13.714-4.571-5.714-3.429-5.714-10.286l9.714-354.857q0-5.714 5.714-10t13.714-4.286h105.714q8 0 13.429 4.286t6 10z" />
+<glyph unicode="&#xf001;" d="M733.714 427.238q0 15.429-10.286 25.714l-258.857 258.857q-10.286 10.286-25.714 10.286t-25.714-10.286l-258.857-258.857q-10.286-10.286-10.286-25.714t10.286-25.714l52-52q10.286-10.286 25.714-10.286t25.714 10.286l108 108v-286.857q0-14.857 10.857-25.714t25.714-10.857h73.143q14.857 0 25.714 10.857t10.857 25.714v286.857l108-108q10.857-10.857 25.714-10.857t25.714 10.857l52 52q10.286 10.286 10.286 25.714zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf002;" d="M665.714 287.81l58.286 58.286q10.857 10.857 10.857 25.714t-10.857 25.714l-259.429 259.429q-10.857 10.857-25.714 10.857t-25.714-10.857l-259.429-259.429q-10.857-10.857-10.857-25.714t10.857-25.714l58.286-58.286q10.857-10.857 25.714-10.857t25.714 10.857l175.429 175.429 175.429-175.429q10.857-10.857 25.714-10.857t25.714 10.857zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf003;" d="M464.571 196.381l259.429 259.429q10.857 10.857 10.857 25.714t-10.857 25.714l-58.286 58.286q-10.857 10.857-25.714 10.857t-25.714-10.857l-175.429-175.429-175.429 175.429q-10.857 10.857-25.714 10.857t-25.714-10.857l-58.286-58.286q-10.857-10.857-10.857-25.714t10.857-25.714l259.429-259.429q10.857-10.857 25.714-10.857t25.714 10.857zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf004;" d="M733.714 426.096q0 15.429-10.286 25.714l-52 52q-10.286 10.286-25.714 10.286t-25.714-10.286l-108-108v286.857q0 14.857-10.857 25.714t-25.714 10.857h-73.143q-14.857 0-25.714-10.857t-10.857-25.714v-286.857l-108 108q-10.857 10.857-25.714 10.857t-25.714-10.857l-52-52q-10.286-10.286-10.286-25.714t10.286-25.714l258.857-258.857q10.286-10.286 25.714-10.286t25.714 10.286l258.857 258.857q10.286 10.286 10.286 25.714zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf010;" d="M438.857 737.524q-84.571 0-156-41.714t-113.143-113.143-41.714-156 41.714-156 113.143-113.143 156-41.714 156 41.714 113.143 113.143 41.714 156-41.714 156-113.143 113.143-156 41.714zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf011;" d="M585.143 426.667q0-60.571-42.857-103.429t-103.429-42.857-103.429 42.857-42.857 103.429 42.857 103.429 103.429 42.857 103.429-42.857 42.857-103.429zM438.857 737.524q-84.571 0-156-41.714t-113.143-113.143-41.714-156 41.714-156 113.143-113.143 156-41.714 156 41.714 113.143 113.143 41.714 156-41.714 156-113.143 113.143-156 41.714zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf012;" d="M438.857 115.81v621.714q-84.571 0-156-41.714t-113.143-113.143-41.714-156 41.714-156 113.143-113.143 156-41.714zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf013;" d="M733.714 519.238q0 16-10.286 26.286l-52 51.429q-10.857 10.857-25.714 10.857t-25.714-10.857l-233.143-232.571-129.143 129.143q-10.857 10.857-25.714 10.857t-25.714-10.857l-52-51.429q-10.286-10.286-10.286-26.286 0-15.429 10.286-25.714l206.857-206.857q10.857-10.857 25.714-10.857 15.429 0 26.286 10.857l310.286 310.286q10.286 10.286 10.286 25.714zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf014;" d="M877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf015;" d="M585.143 426.667q0 60.571-42.857 103.429t-103.429 42.857-103.429-42.857-42.857-103.429 42.857-103.429 103.429-42.857 103.429 42.857 42.857 103.429zM877.714 488.953v-126.857q0-6.857-4.571-13.143t-11.429-7.429l-105.714-16q-10.857-30.857-22.286-52 20-28.571 61.143-78.857 5.714-6.857 5.714-14.286t-5.143-13.143q-15.429-21.143-56.571-61.714t-53.714-40.571q-6.857 0-14.857 5.143l-78.857 61.714q-25.143-13.143-52-21.714-9.143-77.714-16.571-106.286-4-16-20.571-16h-126.857q-8 0-14 4.857t-6.571 12.286l-16 105.143q-28 9.143-51.429 21.143l-80.571-61.143q-5.714-5.143-14.286-5.143-8 0-14.286 6.286-72 65.143-94.286 96-4 5.714-4 13.143 0 6.857 4.571 13.143 8.571 12 29.143 38t30.857 40.286q-15.429 28.571-23.429 56.571l-104.571 15.429q-7.429 1.143-12 7.143t-4.571 13.429v126.857q0 6.857 4.571 13.143t10.857 7.429l106.286 16q8 26.286 22.286 52.571-22.857 32.571-61.143 78.857-5.714 6.857-5.714 13.714 0 5.714 5.143 13.143 14.857 20.571 56.286 61.429t54 40.857q7.429 0 14.857-5.714l78.857-61.143q25.143 13.143 52 21.714 9.143 77.714 16.571 106.286 4 16 20.571 16h126.857q8 0 14-4.857t6.571-12.286l16-105.143q28-9.143 51.429-21.143l81.143 61.143q5.143 5.143 13.714 5.143 7.429 0 14.286-5.714 73.714-68 94.286-97.143 4-4.571 4-12.571 0-6.857-4.571-13.143-8.571-12-29.143-38t-30.857-40.286q14.857-28.571 23.429-56l104.571-16q7.429-1.143 12-7.143t4.571-13.429z" />
+<glyph unicode="&#xf039;" d="M1024 170.667v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 390.096v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 609.524v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 828.953v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714z" />
+<glyph unicode="&#xf05c;" d="M626.857 322.096l-83.429-83.429q-5.714-5.714-13.143-5.714t-13.143 5.714l-78.286 78.286-78.286-78.286q-5.714-5.714-13.143-5.714t-13.143 5.714l-83.429 83.429q-5.714 5.714-5.714 13.143t5.714 13.143l78.286 78.286-78.286 78.286q-5.714 5.714-5.714 13.143t5.714 13.143l83.429 83.429q5.714 5.714 13.143 5.714t13.143-5.714l78.286-78.286 78.286 78.286q5.714 5.714 13.143 5.714t13.143-5.714l83.429-83.429q5.714-5.714 5.714-13.143t-5.714-13.143l-78.286-78.286 78.286-78.286q5.714-5.714 5.714-13.143t-5.714-13.143zM749.714 426.667q0 84.571-41.714 156t-113.143 113.143-156 41.714-156-41.714-113.143-113.143-41.714-156 41.714-156 113.143-113.143 156-41.714 156 41.714 113.143 113.143 41.714 156zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf05d;" d="M669.143 474.096l-241.143-241.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-168 168q-10.857 10.857-10.857 25.714t10.857 25.714l58.286 58.286q10.857 10.857 25.714 10.857t25.714-10.857l84-84 157.143 157.143q10.857 10.857 25.714 10.857t25.714-10.857l58.286-58.286q10.857-10.857 10.857-25.714t-10.857-25.714zM749.714 426.667q0 84.571-41.714 156t-113.143 113.143-156 41.714-156-41.714-113.143-113.143-41.714-156 41.714-156 113.143-113.143 156-41.714 156 41.714 113.143 113.143 41.714 156zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf05e;" d="M749.714 428.381q0 92-49.714 168.571l-430.857-430.286q78.286-50.857 169.714-50.857 63.429 0 120.857 24.857t99.143 66.571 66.286 99.714 24.571 121.429zM178.857 257.524l431.429 430.857q-77.143 52-171.429 52-84.571 0-156-41.714t-113.143-113.714-41.714-156.571q0-92.571 50.857-170.857zM877.714 428.381q0-89.714-34.857-171.429t-93.429-140.571-140-93.714-170.571-34.857-170.571 34.857-140 93.714-93.429 140.571-34.857 171.429 34.857 171.143 93.429 140.286 140 93.714 170.571 34.857 170.571-34.857 140-93.714 93.429-140.286 34.857-171.143z" />
+<glyph unicode="&#xf085;" d="M512 426.667q0 60.571-42.857 103.429t-103.429 42.857-103.429-42.857-42.857-103.429 42.857-103.429 103.429-42.857 103.429 42.857 42.857 103.429zM950.857 134.096q0 29.714-21.714 51.429t-51.429 21.714-51.429-21.714-21.714-51.429q0-30.286 21.429-51.714t51.714-21.429 51.714 21.429 21.429 51.714zM950.857 719.238q0 29.714-21.714 51.429t-51.429 21.714-51.429-21.714-21.714-51.429q0-30.286 21.429-51.714t51.714-21.429 51.714 21.429 21.429 51.714zM731.429 478.667v-105.714q0-5.714-4-11.143t-9.143-6l-88.571-13.714q-6.286-20-18.286-43.429 19.429-27.429 51.429-65.714 4-5.714 4-11.429 0-6.857-4-10.857-13.143-17.143-47.143-51.143t-44.857-34q-6.286 0-12 4l-65.714 51.429q-21.143-10.857-44-17.714-6.286-61.714-13.143-88.571-4-13.714-17.143-13.714h-106.286q-6.286 0-11.429 4.286t-5.714 10l-13.143 87.429q-19.429 5.714-42.857 17.714l-67.429-50.857q-4-4-11.429-4-6.286 0-12 4.571-82.286 76-82.286 91.429 0 5.143 4 10.857 5.714 8 23.429 30.286t26.857 34.857q-13.143 25.143-20 46.857l-86.857 13.714q-5.714 0.571-9.714 5.429t-4 11.143v105.714q0 5.714 4 11.143t9.143 6l88.571 13.714q6.286 20 18.286 43.429-19.429 27.429-51.429 65.714-4 6.286-4 11.429 0 6.857 4 11.429 12.571 17.143 46.857 50.857t45.143 33.714q6.286 0 12-4l65.714-51.429q19.429 10.286 44 18.286 6.286 61.714 13.143 88 4 13.714 17.143 13.714h106.286q6.286 0 11.429-4.286t5.714-10l13.143-87.429q19.429-5.714 42.857-17.714l67.429 50.857q4.571 4 11.429 4 6.286 0 12-4.571 82.286-76 82.286-91.429 0-5.143-4-10.857-6.857-9.143-24-30.857t-25.714-34.286q13.143-27.429 19.429-46.857l86.857-13.143q5.714-1.143 9.714-6t4-11.143zM1097.143 174.096v-80q0-9.143-85.143-17.714-6.857-15.429-17.143-29.714 29.143-64.571 29.143-78.857 0-2.286-2.286-4-69.714-40.571-70.857-40.571-4.571 0-26.286 26.857t-29.714 38.857q-11.429-1.143-17.143-1.143t-17.143 1.143q-8-12-29.714-38.857t-26.286-26.857q-1.143 0-70.857 40.571-2.286 1.714-2.286 4 0 14.286 29.143 78.857-10.286 14.286-17.143 29.714-85.143 8.571-85.143 17.714v80q0 9.143 85.143 17.714 7.429 16.571 17.143 29.714-29.143 64.571-29.143 78.857 0 2.286 2.286 4 2.286 1.143 20 11.429t33.714 19.429 17.143 9.143q4.571 0 26.286-26.571t29.714-38.571q11.429 1.143 17.143 1.143t17.143-1.143q29.143 40.571 52.571 64l3.429 1.143q2.286 0 70.857-40 2.286-1.714 2.286-4 0-14.286-29.143-78.857 9.714-13.143 17.143-29.714 85.143-8.571 85.143-17.714zM1097.143 759.238v-80q0-9.143-85.143-17.714-6.857-15.429-17.143-29.714 29.143-64.571 29.143-78.857 0-2.286-2.286-4-69.714-40.571-70.857-40.571-4.571 0-26.286 26.857t-29.714 38.857q-11.429-1.143-17.143-1.143t-17.143 1.143q-8-12-29.714-38.857t-26.286-26.857q-1.143 0-70.857 40.571-2.286 1.714-2.286 4 0 14.286 29.143 78.857-10.286 14.286-17.143 29.714-85.143 8.571-85.143 17.714v80q0 9.143 85.143 17.714 7.429 16.571 17.143 29.714-29.143 64.571-29.143 78.857 0 2.286 2.286 4 2.286 1.143 20 11.429t33.714 19.429 17.143 9.143q4.571 0 26.286-26.571t29.714-38.571q11.429 1.143 17.143 1.143t17.143-1.143q29.143 40.571 52.571 64l3.429 1.143q2.286 0 70.857-40 2.286-1.714 2.286-4 0-14.286-29.143-78.857 9.714-13.143 17.143-29.714 85.143-8.571 85.143-17.714z" horiz-adv-x="1097" />
+<glyph unicode="&#xf0d7;" d="M585.143 536.381q0-14.857-10.857-25.714l-256-256q-10.857-10.857-25.714-10.857t-25.714 10.857l-256 256q-10.857 10.857-10.857 25.714t10.857 25.714 25.714 10.857h512q14.857 0 25.714-10.857t10.857-25.714z" horiz-adv-x="585" />
+<glyph unicode="&#xf0d8;" d="M585.143 243.81q0-14.857-10.857-25.714t-25.714-10.857h-512q-14.857 0-25.714 10.857t-10.857 25.714 10.857 25.714l256 256q10.857 10.857 25.714 10.857t25.714-10.857l256-256q10.857-10.857 10.857-25.714z" horiz-adv-x="585" />
+<glyph unicode="&#xf0d9;" d="M365.714 682.667v-512q0-14.857-10.857-25.714t-25.714-10.857-25.714 10.857l-256 256q-10.857 10.857-10.857 25.714t10.857 25.714l256 256q10.857 10.857 25.714 10.857t25.714-10.857 10.857-25.714z" horiz-adv-x="366" />
+<glyph unicode="&#xf0da;" d="M329.143 426.667q0-14.857-10.857-25.714l-256-256q-10.857-10.857-25.714-10.857t-25.714 10.857-10.857 25.714v512q0 14.857 10.857 25.714t25.714 10.857 25.714-10.857l256-256q10.857-10.857 10.857-25.714z" horiz-adv-x="366" />
+<glyph unicode="&#xf118;" d="M648 324.381q-21.143-69.143-78.857-111.429t-130.286-42.286-130.286 42.286-78.857 111.429q-4.571 14.286 2.286 27.714t21.714 18q14.286 4.571 27.714-2.286t18-21.714q14.286-45.714 52.857-74t86.571-28.286 86.571 28.286 52.857 74q4.571 14.857 18.286 21.714t28 2.286 21.143-18 2.286-27.714zM365.714 572.953q0-30.286-21.429-51.714t-51.714-21.429-51.714 21.429-21.429 51.714 21.429 51.714 51.714 21.429 51.714-21.429 21.429-51.714zM658.286 572.953q0-30.286-21.429-51.714t-51.714-21.429-51.714 21.429-21.429 51.714 21.429 51.714 51.714 21.429 51.714-21.429 21.429-51.714zM804.571 426.667q0 74.286-29.143 142t-78 116.571-116.571 78-142 29.143-142-29.143-116.571-78-78-116.571-29.143-142 29.143-142 78-116.571 116.571-78 142-29.143 142 29.143 116.571 78 78 116.571 29.143 142zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf119;" d="M648 236.381q4.571-14.286-2.286-27.714t-21.143-18-28 2.286-18.286 21.714q-14.286 45.714-52.857 74t-86.571 28.286-86.571-28.286-52.857-74q-4.571-14.857-18-21.714t-27.714-2.286q-14.857 4.571-21.714 18t-2.286 27.714q21.143 69.143 78.857 111.429t130.286 42.286 130.286-42.286 78.857-111.429zM365.714 572.953q0-30.286-21.429-51.714t-51.714-21.429-51.714 21.429-21.429 51.714 21.429 51.714 51.714 21.429 51.714-21.429 21.429-51.714zM658.286 572.953q0-30.286-21.429-51.714t-51.714-21.429-51.714 21.429-21.429 51.714 21.429 51.714 51.714 21.429 51.714-21.429 21.429-51.714zM804.571 426.667q0 74.286-29.143 142t-78 116.571-116.571 78-142 29.143-142-29.143-116.571-78-78-116.571-29.143-142 29.143-142 78-116.571 116.571-78 142-29.143 142 29.143 116.571 78 78 116.571 29.143 142zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+<glyph unicode="&#xf11a;" d="M658.286 316.953q0-14.857-10.857-25.714t-25.714-10.857h-365.714q-14.857 0-25.714 10.857t-10.857 25.714 10.857 25.714 25.714 10.857h365.714q14.857 0 25.714-10.857t10.857-25.714zM365.714 572.953q0-30.286-21.429-51.714t-51.714-21.429-51.714 21.429-21.429 51.714 21.429 51.714 51.714 21.429 51.714-21.429 21.429-51.714zM658.286 572.953q0-30.286-21.429-51.714t-51.714-21.429-51.714 21.429-21.429 51.714 21.429 51.714 51.714 21.429 51.714-21.429 21.429-51.714zM804.571 426.667q0 74.286-29.143 142t-78 116.571-116.571 78-142 29.143-142-29.143-116.571-78-78-116.571-29.143-142 29.143-142 78-116.571 116.571-78 142-29.143 142 29.143 116.571 78 78 116.571 29.143 142zM877.714 426.667q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
+</font></defs></svg> \ No newline at end of file
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.ttf b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.ttf
new file mode 100755
index 00000000000..5a876906e10
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.ttf
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.woff b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.woff
new file mode 100755
index 00000000000..bf0cf103cd0
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonar.woff
Binary files differ
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonarqube-24x100.png b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonarqube-24x100.png
new file mode 100644
index 00000000000..b2ff23bf288
--- /dev/null
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/scan/report/issuesreport_files/sonarqube-24x100.png
Binary files differ
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java
new file mode 100644
index 00000000000..eb622b003fa
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.PersistenceMode;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.Scopes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DefaultFileLinesContextTest {
+
+ private SonarIndex index;
+ private Resource resource;
+ private DefaultFileLinesContext fileLineMeasures;
+
+ @Before
+ public void setUp() {
+ index = mock(SonarIndex.class);
+ resource = mock(Resource.class);
+ when(resource.getScope()).thenReturn(Scopes.FILE);
+ fileLineMeasures = new DefaultFileLinesContext(index, resource);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotAllowCreationForDirectory() {
+ new DefaultFileLinesContext(index, Directory.create("key"));
+ }
+
+ @Test
+ public void shouldSave() {
+ fileLineMeasures.setIntValue("hits", 1, 2);
+ fileLineMeasures.setIntValue("hits", 3, 4);
+ fileLineMeasures.save();
+
+ assertThat(fileLineMeasures.toString()).isEqualTo("DefaultFileLinesContext{map={hits={1=2, 3=4}}}");
+
+ ArgumentCaptor<Measure> measureCaptor = ArgumentCaptor.forClass(Measure.class);
+ verify(index).addMeasure(Matchers.eq(resource), measureCaptor.capture());
+ Measure measure = measureCaptor.getValue();
+ assertThat(measure.getMetricKey(), is("hits"));
+ assertThat(measure.getPersistenceMode(), is(PersistenceMode.DATABASE));
+ assertThat(measure.getData(), is("1=2;3=4"));
+ }
+
+ @Test
+ public void shouldSaveSeveral() {
+ fileLineMeasures.setIntValue("hits", 1, 2);
+ fileLineMeasures.setIntValue("hits", 3, 4);
+ fileLineMeasures.setStringValue("author", 1, "simon");
+ fileLineMeasures.setStringValue("author", 3, "evgeny");
+ fileLineMeasures.save();
+ fileLineMeasures.setIntValue("branches", 1, 2);
+ fileLineMeasures.setIntValue("branches", 3, 4);
+ fileLineMeasures.save();
+
+ verify(index, times(3)).addMeasure(Matchers.eq(resource), Matchers.any(Measure.class));
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void shouldNotModifyAfterSave() {
+ fileLineMeasures.setIntValue("hits", 1, 2);
+ fileLineMeasures.save();
+ fileLineMeasures.save();
+ verify(index).addMeasure(Matchers.eq(resource), Matchers.any(Measure.class));
+ fileLineMeasures.setIntValue("hits", 1, 2);
+ }
+
+ @Test
+ public void shouldLoadIntValues() {
+ when(index.getMeasure(Matchers.any(Resource.class), Matchers.any(Metric.class)))
+ .thenReturn(new Measure("hits").setData("1=2;3=4"));
+
+ assertThat(fileLineMeasures.getIntValue("hits", 1), is(2));
+ assertThat(fileLineMeasures.getIntValue("hits", 3), is(4));
+ assertThat("no measure on line", fileLineMeasures.getIntValue("hits", 5), nullValue());
+ }
+
+ @Test
+ public void shouldLoadStringValues() {
+ when(index.getMeasure(Matchers.any(Resource.class), Matchers.any(Metric.class)))
+ .thenReturn(new Measure("author").setData("1=simon;3=evgeny"));
+
+ assertThat(fileLineMeasures.getStringValue("author", 1), is("simon"));
+ assertThat(fileLineMeasures.getStringValue("author", 3), is("evgeny"));
+ assertThat("no measure on line", fileLineMeasures.getStringValue("author", 5), nullValue());
+ }
+
+ @Test
+ public void shouldNotSaveAfterLoad() {
+ when(index.getMeasure(Matchers.any(Resource.class), Matchers.any(Metric.class)))
+ .thenReturn(new Measure("author").setData("1=simon;3=evgeny"));
+
+ fileLineMeasures.getStringValue("author", 1);
+ fileLineMeasures.save();
+
+ verify(index, never()).addMeasure(Matchers.eq(resource), Matchers.any(Measure.class));
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void shouldNotModifyAfterLoad() {
+ when(index.getMeasure(Matchers.any(Resource.class), Matchers.any(Metric.class)))
+ .thenReturn(new Measure("author").setData("1=simon;3=evgeny"));
+
+ fileLineMeasures.getStringValue("author", 1);
+ fileLineMeasures.setStringValue("author", 1, "evgeny");
+ }
+
+ @Test
+ public void shouldNotFailIfNoMeasureInIndex() {
+ assertThat(fileLineMeasures.getIntValue("hits", 1), nullValue());
+ assertThat(fileLineMeasures.getStringValue("author", 1), nullValue());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/ProjectConfiguratorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/ProjectConfiguratorTest.java
new file mode 100644
index 00000000000..6a30188454c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/ProjectConfiguratorTest.java
@@ -0,0 +1,130 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch;
+
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.System2;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProjectConfiguratorTest {
+
+ System2 system2;
+
+ @Before
+ public void setUp() {
+ system2 = mock(System2.class);
+ }
+
+ @Test
+ public void analysis_is_today_by_default() {
+ Long now = System.currentTimeMillis();
+ when(system2.now()).thenReturn(now);
+
+ Project project = new Project("key");
+ new ProjectConfigurator(new Settings(), system2).configure(project);
+ assertThat(now - project.getAnalysisDate().getTime()).isLessThan(1000);
+ }
+
+ @Test
+ public void analysis_date_could_be_explicitly_set() {
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_DATE_PROPERTY, "2005-01-30");
+ Project project = new Project("key");
+ new ProjectConfigurator(settings, system2).configure(project);
+
+ assertThat(new SimpleDateFormat("ddMMyyyy").format(project.getAnalysisDate())).isEqualTo("30012005");
+ }
+
+ @Test
+ public void analysis_timestamp_could_be_explicitly_set() {
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_DATE_PROPERTY, "2005-01-30T08:45:10+0000");
+ Project project = new Project("key");
+ new ProjectConfigurator(settings, system2).configure(project);
+
+ SimpleDateFormat dateFormat = new SimpleDateFormat("ddMMyyyy-mmss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+ assertThat(dateFormat.format(project.getAnalysisDate())).isEqualTo("30012005-4510");
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void fail_if_analyis_date_is_not_valid() {
+ Settings configuration = new Settings();
+ configuration.setProperty(CoreProperties.PROJECT_DATE_PROPERTY, "2005/30/01");
+ Project project = new Project("key");
+ new ProjectConfigurator(configuration, system2).configure(project);
+ }
+
+ @Test
+ public void default_analysis_type_is_dynamic() {
+ Project project = new Project("key");
+ new ProjectConfigurator(new Settings(), system2).configure(project);
+ assertThat(project.getAnalysisType()).isEqualTo(Project.AnalysisType.DYNAMIC);
+ }
+
+ @Test
+ public void explicit_dynamic_analysis() {
+ Settings configuration = new Settings();
+ configuration.setProperty(CoreProperties.DYNAMIC_ANALYSIS_PROPERTY, "true");
+ Project project = new Project("key");
+ new ProjectConfigurator(configuration, system2).configure(project);
+ assertThat(project.getAnalysisType()).isEqualTo(Project.AnalysisType.DYNAMIC);
+ }
+
+ @Test
+ public void explicit_static_analysis() {
+ Settings configuration = new Settings();
+ configuration.setProperty(CoreProperties.DYNAMIC_ANALYSIS_PROPERTY, "false");
+ Project project = new Project("key");
+ new ProjectConfigurator(configuration, system2).configure(project);
+ assertThat(project.getAnalysisType()).isEqualTo(Project.AnalysisType.STATIC);
+ }
+
+ @Test
+ public void explicit_dynamic_analysis_reusing_reports() {
+ Settings configuration = new Settings();
+ configuration.setProperty(CoreProperties.DYNAMIC_ANALYSIS_PROPERTY, "reuseReports");
+ Project project = new Project("key");
+ new ProjectConfigurator(configuration, system2).configure(project);
+ assertThat(project.getAnalysisType()).isEqualTo(Project.AnalysisType.REUSE_REPORTS);
+ }
+
+ @Test
+ public void is_dynamic_analysis() {
+ assertThat(Project.AnalysisType.DYNAMIC.isDynamic(false)).isTrue();
+ assertThat(Project.AnalysisType.DYNAMIC.isDynamic(true)).isTrue();
+
+ assertThat(Project.AnalysisType.STATIC.isDynamic(false)).isFalse();
+ assertThat(Project.AnalysisType.STATIC.isDynamic(true)).isFalse();
+
+ assertThat(Project.AnalysisType.REUSE_REPORTS.isDynamic(false)).isFalse();
+ assertThat(Project.AnalysisType.REUSE_REPORTS.isDynamic(true)).isTrue();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisTempFolderProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisTempFolderProviderTest.java
new file mode 100644
index 00000000000..058ea4fd34c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisTempFolderProviderTest.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.analysis;
+
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.junit.Before;
+import org.sonar.batch.analysis.AnalysisTempFolderProvider;
+import org.sonar.api.utils.TempFolder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.mock;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AnalysisTempFolderProviderTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private AnalysisTempFolderProvider tempFolderProvider;
+ private ProjectReactor projectReactor;
+
+ @Before
+ public void setUp() {
+ tempFolderProvider = new AnalysisTempFolderProvider();
+ projectReactor = mock(ProjectReactor.class);
+ ProjectDefinition projectDefinition = mock(ProjectDefinition.class);
+ when(projectReactor.getRoot()).thenReturn(projectDefinition);
+ when(projectDefinition.getWorkDir()).thenReturn(temp.getRoot());
+ }
+
+ @Test
+ public void createTempFolder() throws IOException {
+ File defaultDir = new File(temp.getRoot(), AnalysisTempFolderProvider.TMP_NAME);
+
+ TempFolder tempFolder = tempFolderProvider.provide(projectReactor);
+ tempFolder.newDir();
+ tempFolder.newFile();
+ assertThat(defaultDir).exists();
+ assertThat(defaultDir.list()).hasSize(2);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java
new file mode 100644
index 00000000000..66ab296a063
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.analysis;
+
+import com.google.common.collect.ImmutableMap;
+import org.assertj.core.util.Maps;
+import org.junit.Test;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.home.cache.PersistentCache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class AnalysisWSLoaderProviderTest {
+
+ PersistentCache cache = mock(PersistentCache.class);
+ BatchWsClient wsClient = mock(BatchWsClient.class);
+ AnalysisMode mode = mock(AnalysisMode.class);
+
+ AnalysisWSLoaderProvider underTest = new AnalysisWSLoaderProvider();
+
+ @Test
+ public void testDefault() {
+ WSLoader loader = underTest.provide(mode, cache, wsClient, new AnalysisProperties(Maps.<String, String>newHashMap()));
+ assertThat(loader.getDefaultStrategy()).isEqualTo(LoadStrategy.SERVER_ONLY);
+ }
+
+ @Test
+ public void no_cache_by_default_in_issues_mode() {
+ when(mode.isIssues()).thenReturn(true);
+ WSLoader loader = underTest.provide(mode, cache, wsClient, new AnalysisProperties(Maps.<String, String>newHashMap()));
+ assertThat(loader.getDefaultStrategy()).isEqualTo(LoadStrategy.SERVER_ONLY);
+ }
+
+ @Test
+ public void enable_cache_in_issues_mode() {
+ when(mode.isIssues()).thenReturn(true);
+ WSLoader loader = underTest.provide(mode, cache, wsClient, new AnalysisProperties(ImmutableMap.of(AnalysisWSLoaderProvider.SONAR_USE_WS_CACHE, "true")));
+ assertThat(loader.getDefaultStrategy()).isEqualTo(LoadStrategy.CACHE_ONLY);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/DefaultAnalysisModeTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/DefaultAnalysisModeTest.java
new file mode 100644
index 00000000000..67734ed62fa
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/analysis/DefaultAnalysisModeTest.java
@@ -0,0 +1,141 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.analysis;
+
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.analysis.AnalysisProperties;
+
+import javax.annotation.Nullable;
+
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.junit.Test;
+import org.sonar.api.CoreProperties;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DefaultAnalysisModeTest {
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void regular_analysis_by_default() {
+ DefaultAnalysisMode mode = createMode(null, null);
+ assertThat(mode.isPreview()).isFalse();
+ assertThat(mode.isPublish()).isTrue();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void fail_if_inconsistent() {
+ createMode(null, CoreProperties.ANALYSIS_MODE_ISSUES);
+ }
+
+ @Test
+ public void support_publish_mode() {
+ DefaultAnalysisMode mode = createMode(CoreProperties.ANALYSIS_MODE_PUBLISH);
+
+ assertThat(mode.isPreview()).isFalse();
+ assertThat(mode.isPublish()).isTrue();
+ }
+
+ @Test
+ public void incremental_mode_no_longer_valid() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("This mode was removed in SonarQube 5.2");
+
+ createMode(CoreProperties.ANALYSIS_MODE_INCREMENTAL);
+ }
+
+ @Test
+ public void invalidate_mode() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("[preview, publish, issues]");
+
+ createMode("invalid");
+ }
+
+ @Test
+ public void preview_mode_fallback_issues() {
+ DefaultAnalysisMode mode = createMode(CoreProperties.ANALYSIS_MODE_PREVIEW);
+
+ assertThat(mode.isIssues()).isTrue();
+ assertThat(mode.isPreview()).isFalse();
+ }
+
+ @Test
+ public void scan_all() {
+ Map<String, String> props = new HashMap<>();
+ props.put(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES);
+ GlobalProperties globalProps = new GlobalProperties(props);
+
+ AnalysisProperties analysisProps = new AnalysisProperties(new HashMap<String, String>());
+ DefaultAnalysisMode mode = new DefaultAnalysisMode(globalProps, analysisProps);
+ assertThat(mode.scanAllFiles()).isFalse();
+
+ props.put("sonar.scanAllFiles", "true");
+ analysisProps = new AnalysisProperties(props);
+
+ mode = new DefaultAnalysisMode(globalProps, analysisProps);
+ assertThat(mode.scanAllFiles()).isTrue();
+
+ props.put(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_PUBLISH);
+ analysisProps = new AnalysisProperties(props);
+
+ mode = new DefaultAnalysisMode(globalProps, analysisProps);
+ assertThat(mode.scanAllFiles()).isTrue();
+ }
+
+ @Test
+ public void default_publish_mode() {
+ DefaultAnalysisMode mode = createMode(null);
+ assertThat(mode.isPublish()).isTrue();
+ assertThat(mode.scanAllFiles()).isTrue();
+ }
+
+ @Test
+ public void support_issues_mode() {
+ DefaultAnalysisMode mode = createMode(CoreProperties.ANALYSIS_MODE_ISSUES);
+
+ assertThat(mode.isIssues()).isTrue();
+ assertThat(mode.scanAllFiles()).isFalse();
+ }
+
+ private static DefaultAnalysisMode createMode(@Nullable String mode) {
+ return createMode(mode, mode);
+ }
+
+ private static DefaultAnalysisMode createMode(@Nullable String bootstrapMode, @Nullable String analysisMode) {
+ Map<String, String> bootstrapMap = new HashMap<>();
+ Map<String, String> analysisMap = new HashMap<>();
+
+ if (bootstrapMode != null) {
+ bootstrapMap.put(CoreProperties.ANALYSIS_MODE, bootstrapMode);
+ }
+ if (analysisMode != null) {
+ analysisMap.put(CoreProperties.ANALYSIS_MODE, analysisMode);
+ }
+ return new DefaultAnalysisMode(new GlobalProperties(bootstrapMap), new AnalysisProperties(analysisMap));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchExtensionDictionnaryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchExtensionDictionnaryTest.java
new file mode 100644
index 00000000000..77a0021ef31
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchExtensionDictionnaryTest.java
@@ -0,0 +1,412 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.sonar.api.BatchExtension;
+import org.sonar.api.batch.BuildBreaker;
+import org.sonar.api.batch.CheckProject;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.batch.Phase;
+import org.sonar.api.batch.PostJob;
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.batch.postjob.PostJobContext;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.postjob.PostJobOptimizer;
+import org.sonar.batch.sensor.DefaultSensorContext;
+import org.sonar.batch.sensor.SensorOptimizer;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+public class BatchExtensionDictionnaryTest {
+
+ private BatchExtensionDictionnary newSelector(Object... extensions) {
+ ComponentContainer iocContainer = new ComponentContainer();
+ for (Object extension : extensions) {
+ iocContainer.addSingleton(extension);
+ }
+ return new BatchExtensionDictionnary(iocContainer, mock(DefaultSensorContext.class), mock(SensorOptimizer.class), mock(PostJobContext.class),
+ mock(PostJobOptimizer.class));
+ }
+
+ @Test
+ public void testGetFilteredExtensionWithExtensionMatcher() {
+ final Sensor sensor1 = new FakeSensor();
+ final Sensor sensor2 = new FakeSensor();
+
+ BatchExtensionDictionnary selector = newSelector(sensor1, sensor2);
+ Collection<Sensor> sensors = selector.select(Sensor.class, null, true, new ExtensionMatcher() {
+ @Override
+ public boolean accept(Object extension) {
+ return extension.equals(sensor1);
+ }
+ });
+
+ assertThat(sensors).contains(sensor1);
+ assertEquals(1, sensors.size());
+ }
+
+ @Test
+ public void testGetFilteredExtensions() {
+ Sensor sensor1 = new FakeSensor();
+ Sensor sensor2 = new FakeSensor();
+ Decorator decorator = mock(Decorator.class);
+
+ BatchExtensionDictionnary selector = newSelector(sensor1, sensor2, decorator);
+ Collection<Sensor> sensors = selector.select(Sensor.class, null, true, null);
+
+ assertThat(sensors).containsOnly(sensor1, sensor2);
+ }
+
+ @Test
+ public void shouldSearchInParentContainers() {
+ Sensor a = new FakeSensor();
+ Sensor b = new FakeSensor();
+ Sensor c = new FakeSensor();
+
+ ComponentContainer grandParent = new ComponentContainer();
+ grandParent.addSingleton(a);
+
+ ComponentContainer parent = grandParent.createChild();
+ parent.addSingleton(b);
+
+ ComponentContainer child = parent.createChild();
+ child.addSingleton(c);
+
+ BatchExtensionDictionnary dictionnary = new BatchExtensionDictionnary(child, mock(DefaultSensorContext.class), mock(SensorOptimizer.class), mock(PostJobContext.class),
+ mock(PostJobOptimizer.class));
+ assertThat(dictionnary.select(Sensor.class, null, true, null)).containsOnly(a, b, c);
+ }
+
+ @Test
+ public void sortExtensionsByDependency() {
+ BatchExtension a = new MethodDependentOf(null);
+ BatchExtension b = new MethodDependentOf(a);
+ BatchExtension c = new MethodDependentOf(b);
+
+ BatchExtensionDictionnary selector = newSelector(b, c, a);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(3);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+ assertThat(extensions.get(2)).isEqualTo(c);
+ }
+
+ @Test
+ public void useMethodAnnotationsToSortExtensions() {
+ BatchExtension a = new GeneratesSomething("foo");
+ BatchExtension b = new MethodDependentOf("foo");
+
+ BatchExtensionDictionnary selector = newSelector(a, b);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions.size()).isEqualTo(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+
+ // different initial order
+ selector = newSelector(b, a);
+ extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+ }
+
+ @Test
+ public void methodDependsUponCollection() {
+ BatchExtension a = new GeneratesSomething("foo");
+ BatchExtension b = new MethodDependentOf(Arrays.asList("foo"));
+
+ BatchExtensionDictionnary selector = newSelector(a, b);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+
+ // different initial order
+ selector = newSelector(b, a);
+ extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+ }
+
+ @Test
+ public void methodDependsUponArray() {
+ BatchExtension a = new GeneratesSomething("foo");
+ BatchExtension b = new MethodDependentOf(new String[] {"foo"});
+
+ BatchExtensionDictionnary selector = newSelector(a, b);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+
+ // different initial order
+ selector = newSelector(b, a);
+ extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+ }
+
+ @Test
+ public void useClassAnnotationsToSortExtensions() {
+ BatchExtension a = new ClassDependedUpon();
+ BatchExtension b = new ClassDependsUpon();
+
+ BatchExtensionDictionnary selector = newSelector(a, b);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+
+ // different initial order
+ selector = newSelector(b, a);
+ extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+ }
+
+ @Test
+ public void useClassAnnotationsOnInterfaces() {
+ BatchExtension a = new InterfaceDependedUpon() {
+ };
+ BatchExtension b = new InterfaceDependsUpon() {
+ };
+
+ BatchExtensionDictionnary selector = newSelector(a, b);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+
+ // different initial order
+ selector = newSelector(b, a);
+ extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+ }
+
+ @Test
+ public void checkProject() {
+ BatchExtension ok = new CheckProjectOK();
+ BatchExtension ko = new CheckProjectKO();
+
+ BatchExtensionDictionnary selector = newSelector(ok, ko);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, new Project("key"), true, null));
+
+ assertThat(extensions).hasSize(1);
+ assertThat(extensions.get(0)).isInstanceOf(CheckProjectOK.class);
+ }
+
+ @Test
+ public void inheritAnnotations() {
+ BatchExtension a = new SubClass("foo");
+ BatchExtension b = new MethodDependentOf("foo");
+
+ BatchExtensionDictionnary selector = newSelector(b, a);
+ List<BatchExtension> extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+
+ // change initial order
+ selector = newSelector(a, b);
+ extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(2);
+ assertThat(extensions.get(0)).isEqualTo(a);
+ assertThat(extensions.get(1)).isEqualTo(b);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void annotatedMethodsCanNotBePrivate() {
+ BatchExtensionDictionnary selector = newSelector();
+ BatchExtension wrong = new BatchExtension() {
+ @DependsUpon
+ private Object foo() {
+ return "foo";
+ }
+ };
+ selector.evaluateAnnotatedClasses(wrong, DependsUpon.class);
+ }
+
+ @Test
+ public void dependsUponPhase() {
+ BatchExtension pre = new PreSensor();
+ BatchExtension analyze = new GeneratesSomething("something");
+ BatchExtension post = new PostSensor();
+
+ BatchExtensionDictionnary selector = newSelector(analyze, post, pre);
+ List extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(3);
+ assertThat(extensions.get(0)).isEqualTo(pre);
+ assertThat(extensions.get(1)).isEqualTo(analyze);
+ assertThat(extensions.get(2)).isEqualTo(post);
+ }
+
+ @Test
+ public void dependsUponInheritedPhase() {
+ BatchExtension pre = new PreSensorSubclass();
+ BatchExtension analyze = new GeneratesSomething("something");
+ BatchExtension post = new PostSensorSubclass();
+
+ BatchExtensionDictionnary selector = newSelector(analyze, post, pre);
+ List extensions = Lists.newArrayList(selector.select(BatchExtension.class, null, true, null));
+
+ assertThat(extensions).hasSize(3);
+ assertThat(extensions.get(0)).isEqualTo(pre);
+ assertThat(extensions.get(1)).isEqualTo(analyze);
+ assertThat(extensions.get(2)).isEqualTo(post);
+ }
+
+ @Test
+ public void buildStatusCheckersAreExecutedAfterOtherPostJobs() {
+ BuildBreaker checker = new BuildBreaker() {
+ public void executeOn(Project project, SensorContext context) {
+ }
+ };
+
+ BatchExtensionDictionnary selector = newSelector(new FakePostJob(), checker, new FakePostJob());
+ List extensions = Lists.newArrayList(selector.select(PostJob.class, null, true, null));
+
+ assertThat(extensions).hasSize(3);
+ assertThat(extensions.get(2)).isEqualTo(checker);
+ }
+
+ class FakeSensor implements Sensor {
+
+ public void analyse(Project project, SensorContext context) {
+
+ }
+
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+ }
+
+ class MethodDependentOf implements BatchExtension {
+ private Object dep;
+
+ MethodDependentOf(Object o) {
+ this.dep = o;
+ }
+
+ @DependsUpon
+ public Object dependsUponObject() {
+ return dep;
+ }
+ }
+
+ @DependsUpon("flag")
+ class ClassDependsUpon implements BatchExtension {
+ }
+
+ @DependedUpon("flag")
+ class ClassDependedUpon implements BatchExtension {
+ }
+
+ @DependsUpon("flag")
+ interface InterfaceDependsUpon extends BatchExtension {
+ }
+
+ @DependedUpon("flag")
+ interface InterfaceDependedUpon extends BatchExtension {
+ }
+
+ class GeneratesSomething implements BatchExtension {
+ private Object gen;
+
+ GeneratesSomething(Object o) {
+ this.gen = o;
+ }
+
+ @DependedUpon
+ public Object generates() {
+ return gen;
+ }
+ }
+
+ class SubClass extends GeneratesSomething {
+ SubClass(Object o) {
+ super(o);
+ }
+ }
+
+ @Phase(name = Phase.Name.PRE)
+ class PreSensor implements BatchExtension {
+
+ }
+
+ class PreSensorSubclass extends PreSensor {
+
+ }
+
+ @Phase(name = Phase.Name.POST)
+ class PostSensor implements BatchExtension {
+
+ }
+
+ class PostSensorSubclass extends PostSensor {
+
+ }
+
+ class CheckProjectOK implements BatchExtension, CheckProject {
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+ }
+
+ class CheckProjectKO implements BatchExtension, CheckProject {
+ public boolean shouldExecuteOnProject(Project project) {
+ return false;
+ }
+ }
+
+ private class FakePostJob implements PostJob {
+ public void executeOn(Project project, SensorContext context) {
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java
new file mode 100644
index 00000000000..95e17ca849a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.io.File;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.core.platform.RemotePlugin;
+import org.sonar.home.cache.FileCache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BatchPluginInstallerTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ FileCache fileCache = mock(FileCache.class);
+ BatchWsClient wsClient = mock(BatchWsClient.class);
+ BatchPluginPredicate pluginPredicate = mock(BatchPluginPredicate.class);
+
+ @Test
+ public void listRemotePlugins() {
+
+ WSLoader wsLoader = mock(WSLoader.class);
+ when(wsLoader.loadString("/deploy/plugins/index.txt")).thenReturn(new WSLoaderResult<>("checkstyle\nsqale", true));
+ BatchPluginInstaller underTest = new BatchPluginInstaller(wsLoader, wsClient, fileCache, pluginPredicate);
+
+ List<RemotePlugin> remotePlugins = underTest.listRemotePlugins();
+ assertThat(remotePlugins).extracting("key").containsOnly("checkstyle", "sqale");
+ }
+
+ @Test
+ public void should_download_plugin() throws Exception {
+ File pluginJar = temp.newFile();
+ when(fileCache.get(eq("checkstyle-plugin.jar"), eq("fakemd5_1"), any(FileCache.Downloader.class))).thenReturn(pluginJar);
+
+ WSLoader wsLoader = mock(WSLoader.class);
+ BatchPluginInstaller underTest = new BatchPluginInstaller(wsLoader, wsClient, fileCache, pluginPredicate);
+
+ RemotePlugin remote = new RemotePlugin("checkstyle").setFile("checkstyle-plugin.jar", "fakemd5_1");
+ File file = underTest.download(remote);
+
+ assertThat(file).isEqualTo(pluginJar);
+ }
+
+ @Test
+ public void should_fail_to_get_plugin_index() {
+ thrown.expect(IllegalStateException.class);
+
+ WSLoader wsLoader = mock(WSLoader.class);
+ doThrow(new IllegalStateException()).when(wsLoader).loadString("/deploy/plugins/index.txt");
+
+ new BatchPluginInstaller(wsLoader, wsClient, fileCache, pluginPredicate).installRemotes();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginJarExploderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginJarExploderTest.java
new file mode 100644
index 00000000000..fe991f5d406
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginJarExploderTest.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.core.platform.ExplodedPlugin;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.home.cache.FileCache;
+import org.sonar.home.cache.FileCacheBuilder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BatchPluginJarExploderTest {
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ File userHome;
+ BatchPluginJarExploder underTest;
+
+ @Before
+ public void setUp() throws IOException {
+ userHome = temp.newFolder();
+ FileCache fileCache = new FileCacheBuilder(new Slf4jLogger()).setUserHome(userHome).build();
+ underTest = new BatchPluginJarExploder(fileCache);
+ }
+
+ @Test
+ public void copy_and_extract_libs() throws IOException {
+ File fileFromCache = getFileFromCache("sonar-checkstyle-plugin-2.8.jar");
+ ExplodedPlugin exploded = underTest.explode(PluginInfo.create(fileFromCache));
+
+ assertThat(exploded.getKey()).isEqualTo("checkstyle");
+ assertThat(exploded.getMain()).isFile().exists();
+ assertThat(exploded.getLibs()).extracting("name").containsOnly("antlr-2.7.6.jar", "checkstyle-5.1.jar", "commons-cli-1.0.jar");
+ assertThat(new File(fileFromCache.getParent(), "sonar-checkstyle-plugin-2.8.jar")).exists();
+ assertThat(new File(fileFromCache.getParent(), "sonar-checkstyle-plugin-2.8.jar_unzip/META-INF/lib/checkstyle-5.1.jar")).exists();
+ }
+
+ @Test
+ public void extract_only_libs() throws IOException {
+ File fileFromCache = getFileFromCache("sonar-checkstyle-plugin-2.8.jar");
+ underTest.explode(PluginInfo.create(fileFromCache));
+
+ assertThat(new File(fileFromCache.getParent(), "sonar-checkstyle-plugin-2.8.jar")).exists();
+ assertThat(new File(fileFromCache.getParent(), "sonar-checkstyle-plugin-2.8.jar_unzip/META-INF/MANIFEST.MF")).doesNotExist();
+ assertThat(new File(fileFromCache.getParent(), "sonar-checkstyle-plugin-2.8.jar_unzip/org/sonar/plugins/checkstyle/CheckstyleVersion.class")).doesNotExist();
+ }
+
+ File getFileFromCache(String filename) throws IOException {
+ File src = FileUtils.toFile(getClass().getResource(this.getClass().getSimpleName() + "/" + filename));
+ File destFile = new File(new File(userHome, "" + filename.hashCode()), filename);
+ FileUtils.copyFile(src, destFile);
+ return destFile;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginPredicateTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginPredicateTest.java
new file mode 100644
index 00000000000..0e8f29a1609
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginPredicateTest.java
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.junit.Test;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BatchPluginPredicateTest {
+
+ Settings settings = new Settings();
+ GlobalMode mode = mock(GlobalMode.class);
+
+ @Test
+ public void accept_if_no_inclusions_nor_exclusions() {
+ BatchPluginPredicate predicate = new BatchPluginPredicate(settings, mode);
+ assertThat(predicate.getWhites()).isEmpty();
+ assertThat(predicate.getBlacks()).isEmpty();
+ assertThat(predicate.apply("pmd")).isTrue();
+ assertThat(predicate.apply("buildbreaker")).isTrue();
+ }
+
+ @Test
+ public void exclude_buildbreaker_in_preview_mode() {
+ when(mode.isPreview()).thenReturn(true);
+ BatchPluginPredicate predicate = new BatchPluginPredicate(settings, mode);
+ assertThat(predicate.apply("buildbreaker")).isFalse();
+ }
+
+ @Test
+ public void inclusions_take_precedence_over_exclusions() {
+ when(mode.isPreview()).thenReturn(true);
+ settings
+ .setProperty(CoreProperties.PREVIEW_INCLUDE_PLUGINS, "checkstyle,pmd,findbugs")
+ .setProperty(CoreProperties.PREVIEW_EXCLUDE_PLUGINS, "cobertura,pmd");
+ BatchPluginPredicate predicate = new BatchPluginPredicate(settings, mode);
+ assertThat(predicate.apply("pmd")).isTrue();
+ }
+
+ @Test
+ public void verify_both_inclusions_and_exclusions() {
+ when(mode.isPreview()).thenReturn(true);
+ settings
+ .setProperty(CoreProperties.PREVIEW_INCLUDE_PLUGINS, "checkstyle,pmd,findbugs")
+ .setProperty(CoreProperties.PREVIEW_EXCLUDE_PLUGINS, "cobertura");
+ BatchPluginPredicate predicate = new BatchPluginPredicate(settings, mode);
+ assertThat(predicate.apply("checkstyle")).isTrue();
+ assertThat(predicate.apply("pmd")).isTrue();
+ assertThat(predicate.apply("cobertura")).isFalse();
+ }
+
+ @Test
+ public void verify_both_inclusions_and_exclusions_issues() {
+ when(mode.isIssues()).thenReturn(true);
+ settings
+ .setProperty(CoreProperties.PREVIEW_INCLUDE_PLUGINS, "checkstyle,pmd,findbugs")
+ .setProperty(CoreProperties.PREVIEW_EXCLUDE_PLUGINS, "cobertura");
+ BatchPluginPredicate predicate = new BatchPluginPredicate(settings, mode);
+ assertThat(predicate.apply("checkstyle")).isTrue();
+ assertThat(predicate.apply("pmd")).isTrue();
+ assertThat(predicate.apply("cobertura")).isFalse();
+ }
+
+ @Test
+ public void test_exclusions_without_any_inclusions() {
+ when(mode.isPreview()).thenReturn(true);
+ settings.setProperty(CoreProperties.PREVIEW_EXCLUDE_PLUGINS, "checkstyle,pmd,findbugs");
+ BatchPluginPredicate predicate = new BatchPluginPredicate(settings, mode);
+ assertThat(predicate.apply("checkstyle")).isFalse();
+ assertThat(predicate.apply("pmd")).isFalse();
+ assertThat(predicate.apply("cobertura")).isTrue();
+ }
+
+ @Test
+ public void trim_inclusions_and_exclusions() {
+ settings
+ .setProperty(CoreProperties.PREVIEW_INCLUDE_PLUGINS, "checkstyle, pmd, findbugs")
+ .setProperty(CoreProperties.PREVIEW_EXCLUDE_PLUGINS, "cobertura, pmd");
+ BatchPluginPredicate predicate = new BatchPluginPredicate(settings, mode);
+ assertThat(predicate.apply("pmd")).isTrue();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginRepositoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginRepositoryTest.java
new file mode 100644
index 00000000000..0d0fc068dfe
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchPluginRepositoryTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Test;
+import org.sonar.api.SonarPlugin;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyCollectionOf;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class BatchPluginRepositoryTest {
+
+ PluginInstaller installer = mock(PluginInstaller.class);
+ PluginLoader loader = mock(PluginLoader.class);
+ BatchPluginRepository underTest = new BatchPluginRepository(installer, loader);
+
+ @Test
+ public void install_and_load_plugins() {
+ PluginInfo info = new PluginInfo("squid");
+ ImmutableMap<String, PluginInfo> infos = ImmutableMap.of("squid", info);
+ SonarPlugin instance = mock(SonarPlugin.class);
+ when(loader.load(infos)).thenReturn(ImmutableMap.of("squid", instance));
+ when(installer.installRemotes()).thenReturn(infos);
+
+ underTest.start();
+
+ assertThat(underTest.getPluginInfos()).containsOnly(info);
+ assertThat(underTest.getPluginInfo("squid")).isSameAs(info);
+ assertThat(underTest.getPluginInstance("squid")).isSameAs(instance);
+
+ underTest.stop();
+ verify(loader).unload(anyCollectionOf(SonarPlugin.class));
+ }
+
+ @Test
+ public void fail_if_requesting_missing_plugin() {
+ underTest.start();
+
+ try {
+ underTest.getPluginInfo("unknown");
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Plugin [unknown] does not exist");
+ }
+ try {
+ underTest.getPluginInstance("unknown");
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Plugin [unknown] does not exist");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientProviderTest.java
new file mode 100644
index 00000000000..76fa07167ab
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientProviderTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.sonar.batch.bootstrapper.EnvironmentInformation;
+import org.sonarqube.ws.client.HttpConnector;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BatchWsClientProviderTest {
+
+ BatchWsClientProvider underTest = new BatchWsClientProvider();
+ EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3");
+
+ @Test
+ public void provide_client_with_default_settings() {
+ GlobalProperties settings = new GlobalProperties(new HashMap<String, String>());
+
+ BatchWsClient client = underTest.provide(settings, env);
+
+ assertThat(client).isNotNull();
+ assertThat(client.baseUrl()).isEqualTo("http://localhost:9000/");
+ HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+ assertThat(httpConnector.baseUrl()).isEqualTo("http://localhost:9000/");
+ assertThat(httpConnector.okHttpClient().getProxy()).isNull();
+ assertThat(httpConnector.okHttpClient().getConnectTimeout()).isEqualTo(5_000);
+ assertThat(httpConnector.okHttpClient().getReadTimeout()).isEqualTo(60_000);
+ assertThat(httpConnector.userAgent()).isEqualTo("Maven Plugin/2.3");
+ }
+
+ @Test
+ public void provide_client_with_custom_settings() {
+ Map<String, String> props = new HashMap<>();
+ props.put("sonar.host.url", "https://here/sonarqube");
+ props.put("sonar.login", "theLogin");
+ props.put("sonar.password", "thePassword");
+ props.put("sonar.ws.timeout", "42");
+ GlobalProperties settings = new GlobalProperties(props);
+
+ BatchWsClient client = underTest.provide(settings, env);
+
+ assertThat(client).isNotNull();
+ HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+ assertThat(httpConnector.baseUrl()).isEqualTo("https://here/sonarqube/");
+ assertThat(httpConnector.okHttpClient().getProxy()).isNull();
+ assertThat(httpConnector.userAgent()).isEqualTo("Maven Plugin/2.3");
+ }
+
+ @Test
+ public void build_singleton() {
+ GlobalProperties settings = new GlobalProperties(new HashMap<String, String>());
+ BatchWsClient first = underTest.provide(settings, env);
+ BatchWsClient second = underTest.provide(settings, env);
+ assertThat(first).isSameAs(second);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientTest.java
new file mode 100644
index 00000000000..3689c2d601a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/BatchWsClientTest.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.MockWsResponse;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BatchWsClientTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ WsClient wsClient = mock(WsClient.class, Mockito.RETURNS_DEEP_STUBS);
+
+ @Test
+ public void log_and_profile_request_if_debug_level() throws Exception {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse().setRequestUrl("https://local/api/issues/search");
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ logTester.setLevel(LoggerLevel.DEBUG);
+ BatchWsClient underTest = new BatchWsClient(wsClient, false);
+
+ WsResponse result = underTest.call(request);
+
+ // do not fail the execution -> interceptor returns the response
+ assertThat(result).isSameAs(response);
+
+ // check logs
+ List<String> debugLogs = logTester.logs(LoggerLevel.DEBUG);
+ assertThat(debugLogs).hasSize(1);
+ assertThat(debugLogs.get(0)).contains("GET 200 https://local/api/issues/search | time=");
+ }
+
+ @Test
+ public void fail_if_requires_credentials() throws Exception {
+ expectedException.expect(MessageException.class);
+ expectedException
+ .expectMessage("Not authorized. Analyzing this project requires to be authenticated. Please provide the values of the properties sonar.login and sonar.password.");
+
+ WsRequest request = newRequest();
+ WsResponse response = newResponse().setCode(401);
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ new BatchWsClient(wsClient, false).call(request);
+ }
+
+ @Test
+ public void fail_if_credentials_are_not_valid() throws Exception {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Not authorized. Please check the properties sonar.login and sonar.password.");
+
+ WsRequest request = newRequest();
+ WsResponse response = newResponse().setCode(401);
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ new BatchWsClient(wsClient, /* credentials are configured */true).call(request);
+ }
+
+ @Test
+ public void fail_if_requires_permission() throws Exception {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("missing scan permission, missing another permission");
+
+ WsRequest request = newRequest();
+ WsResponse response = newResponse()
+ .setCode(403)
+ .setContent("{\"errors\":[{\"msg\":\"missing scan permission\"}, {\"msg\":\"missing another permission\"}]}");
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ new BatchWsClient(wsClient, true).call(request);
+ }
+
+ private MockWsResponse newResponse() {
+ return new MockWsResponse().setRequestUrl("https://local/api/issues/search");
+ }
+
+ private WsRequest newRequest() {
+ return new GetRequest("api/issues/search");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/DroppedPropertyCheckerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/DroppedPropertyCheckerTest.java
new file mode 100644
index 00000000000..e6cd758d253
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/DroppedPropertyCheckerTest.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DroppedPropertyCheckerTest {
+ private static final String SOME_VALUE = "some value";
+ private static final String DROPPED_PROPERTY_1 = "I'm dropped";
+ private static final String DROPPED_PROPERTY_MSG_1 = "blablabla!";
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Test
+ public void no_log_if_no_dropped_property() {
+ new DroppedPropertyChecker(ImmutableMap.of(DROPPED_PROPERTY_1, SOME_VALUE), ImmutableMap.<String, String>of()).checkDroppedProperties();
+
+ assertThat(logTester.logs()).isEmpty();
+ }
+
+ @Test
+ public void no_log_if_settings_does_not_contain_any_dropped_property() {
+ new DroppedPropertyChecker(ImmutableMap.<String, String>of(), ImmutableMap.of(DROPPED_PROPERTY_1, DROPPED_PROPERTY_MSG_1)).checkDroppedProperties();
+
+ assertThat(logTester.logs()).isEmpty();
+ }
+
+ @Test
+ public void warn_log_if_settings_contains_any_dropped_property() {
+ new DroppedPropertyChecker(ImmutableMap.of(DROPPED_PROPERTY_1, SOME_VALUE), ImmutableMap.of(DROPPED_PROPERTY_1, DROPPED_PROPERTY_MSG_1)).checkDroppedProperties();
+
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly("Property '" + DROPPED_PROPERTY_1 + "' is not supported any more. " + DROPPED_PROPERTY_MSG_1);
+ assertThat(logTester.logs(LoggerLevel.INFO)).isEmpty();
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty();
+ assertThat(logTester.logs(LoggerLevel.TRACE)).isEmpty();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionInstallerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionInstallerTest.java
new file mode 100644
index 00000000000..c368fb146b6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionInstallerTest.java
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.commons.lang.ClassUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.BatchExtension;
+import org.sonar.api.ExtensionProvider;
+import org.sonar.api.SonarPlugin;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.core.platform.ComponentContainer;
+import org.sonar.core.platform.PluginInfo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ExtensionInstallerTest {
+
+ GlobalMode mode;
+ BatchPluginRepository pluginRepository = mock(BatchPluginRepository.class);
+
+ private static SonarPlugin newPluginInstance(final Object... extensions) {
+ return new SonarPlugin() {
+ public List getExtensions() {
+ return Arrays.asList(extensions);
+ }
+ };
+ }
+
+ @Before
+ public void setUp() {
+ mode = mock(GlobalMode.class);
+ }
+
+ @Test
+ public void should_filter_extensions_to_install() {
+ when(pluginRepository.getPluginInfos()).thenReturn(Arrays.asList(new PluginInfo("foo")));
+ when(pluginRepository.getPluginInstance("foo")).thenReturn(newPluginInstance(Foo.class, Bar.class));
+
+ ComponentContainer container = new ComponentContainer();
+ ExtensionInstaller installer = new ExtensionInstaller(pluginRepository, mock(AnalysisMode.class));
+ installer.install(container, new FooMatcher());
+
+ assertThat(container.getComponentByType(Foo.class)).isNotNull();
+ assertThat(container.getComponentByType(Bar.class)).isNull();
+ }
+
+ @Test
+ public void should_execute_extension_provider() {
+ when(pluginRepository.getPluginInfos()).thenReturn(Arrays.asList(new PluginInfo("foo")));
+ when(pluginRepository.getPluginInstance("foo")).thenReturn(newPluginInstance(new FooProvider(), new BarProvider()));
+ ComponentContainer container = new ComponentContainer();
+ ExtensionInstaller installer = new ExtensionInstaller(pluginRepository, mock(AnalysisMode.class));
+
+ installer.install(container, new FooMatcher());
+
+ assertThat(container.getComponentByType(Foo.class)).isNotNull();
+ assertThat(container.getComponentByType(Bar.class)).isNull();
+ }
+
+ @Test
+ public void should_provide_list_of_extensions() {
+ when(pluginRepository.getPluginInfos()).thenReturn(Arrays.asList(new PluginInfo("foo")));
+ when(pluginRepository.getPluginInstance("foo")).thenReturn(newPluginInstance(new FooBarProvider()));
+ ComponentContainer container = new ComponentContainer();
+ ExtensionInstaller installer = new ExtensionInstaller(pluginRepository, mock(AnalysisMode.class));
+
+ installer.install(container, new TrueMatcher());
+
+ assertThat(container.getComponentByType(Foo.class)).isNotNull();
+ assertThat(container.getComponentByType(Bar.class)).isNotNull();
+ }
+
+ private static class FooMatcher implements ExtensionMatcher {
+ public boolean accept(Object extension) {
+ return extension.equals(Foo.class) || ClassUtils.isAssignable(Foo.class, extension.getClass()) || ClassUtils.isAssignable(FooProvider.class, extension.getClass());
+ }
+ }
+
+ private static class TrueMatcher implements ExtensionMatcher {
+ public boolean accept(Object extension) {
+ return true;
+ }
+ }
+
+ public static class Foo implements BatchExtension {
+
+ }
+
+ public static class Bar implements BatchExtension {
+
+ }
+
+ public static class FooProvider extends ExtensionProvider implements BatchExtension {
+ @Override
+ public Object provide() {
+ return new Foo();
+ }
+ }
+
+ public static class BarProvider extends ExtensionProvider implements BatchExtension {
+ @Override
+ public Object provide() {
+ return new Bar();
+ }
+ }
+
+ public static class FooBarProvider extends ExtensionProvider implements BatchExtension {
+ @Override
+ public Object provide() {
+ return Arrays.asList(new Foo(), new Bar());
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionUtilsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionUtilsTest.java
new file mode 100644
index 00000000000..f83920847e3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/ExtensionUtilsTest.java
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.junit.Test;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.server.ServerSide;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExtensionUtilsTest {
+
+ @Test
+ public void shouldBeBatchInstantiationStrategy() {
+ assertThat(ExtensionUtils.isInstantiationStrategy(BatchService.class, InstantiationStrategy.PER_BATCH)).isTrue();
+ assertThat(ExtensionUtils.isInstantiationStrategy(new BatchService(), InstantiationStrategy.PER_BATCH)).isTrue();
+ assertThat(ExtensionUtils.isInstantiationStrategy(ProjectService.class, InstantiationStrategy.PER_BATCH)).isFalse();
+ assertThat(ExtensionUtils.isInstantiationStrategy(new ProjectService(), InstantiationStrategy.PER_BATCH)).isFalse();
+ assertThat(ExtensionUtils.isInstantiationStrategy(DefaultService.class, InstantiationStrategy.PER_BATCH)).isFalse();
+ assertThat(ExtensionUtils.isInstantiationStrategy(new DefaultService(), InstantiationStrategy.PER_BATCH)).isFalse();
+ }
+
+ @Test
+ public void shouldBeProjectInstantiationStrategy() {
+ assertThat(ExtensionUtils.isInstantiationStrategy(BatchService.class, InstantiationStrategy.PER_PROJECT)).isFalse();
+ assertThat(ExtensionUtils.isInstantiationStrategy(new BatchService(), InstantiationStrategy.PER_PROJECT)).isFalse();
+ assertThat(ExtensionUtils.isInstantiationStrategy(ProjectService.class, InstantiationStrategy.PER_PROJECT)).isTrue();
+ assertThat(ExtensionUtils.isInstantiationStrategy(new ProjectService(), InstantiationStrategy.PER_PROJECT)).isTrue();
+ assertThat(ExtensionUtils.isInstantiationStrategy(DefaultService.class, InstantiationStrategy.PER_PROJECT)).isTrue();
+ assertThat(ExtensionUtils.isInstantiationStrategy(new DefaultService(), InstantiationStrategy.PER_PROJECT)).isTrue();
+ }
+
+ @Test
+ public void testIsBatchSide() {
+ assertThat(ExtensionUtils.isBatchSide(BatchService.class)).isTrue();
+ assertThat(ExtensionUtils.isBatchSide(new BatchService())).isTrue();
+ assertThat(ExtensionUtils.isBatchSide(DeprecatedBatchService.class)).isTrue();
+
+ assertThat(ExtensionUtils.isBatchSide(ServerService.class)).isFalse();
+ assertThat(ExtensionUtils.isBatchSide(new ServerService())).isFalse();
+ }
+
+ @BatchSide
+ @InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+ public static class BatchService {
+
+ }
+
+ public static class DeprecatedBatchService implements BatchComponent {
+
+ }
+
+ @BatchSide
+ @InstantiationStrategy(InstantiationStrategy.PER_PROJECT)
+ public static class ProjectService {
+
+ }
+
+ @BatchSide
+ public static class DefaultService {
+
+ }
+
+ @ServerSide
+ public static class ServerService {
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/FileCacheProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/FileCacheProviderTest.java
new file mode 100644
index 00000000000..09ce7835e0c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/FileCacheProviderTest.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.home.cache.FileCache;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FileCacheProviderTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void provide() {
+ FileCacheProvider provider = new FileCacheProvider();
+ FileCache cache = provider.provide(new Settings());
+
+ assertThat(cache).isNotNull();
+ assertThat(cache.getDir()).isNotNull().exists();
+ }
+
+ @Test
+ public void keep_singleton_instance() {
+ FileCacheProvider provider = new FileCacheProvider();
+ Settings settings = new Settings();
+ FileCache cache1 = provider.provide(settings);
+ FileCache cache2 = provider.provide(settings);
+
+ assertThat(cache1).isSameAs(cache2);
+ }
+
+ @Test
+ public void honor_sonarUserHome() throws IOException {
+ FileCacheProvider provider = new FileCacheProvider();
+ Settings settings = new Settings();
+ File f = temp.newFolder();
+ settings.appendProperty("sonar.userHome", f.getAbsolutePath());
+ FileCache cache = provider.provide(settings);
+
+ assertThat(cache.getDir()).isEqualTo(new File(f, "cache"));
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalContainerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalContainerTest.java
new file mode 100644
index 00000000000..41152b3ccb5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalContainerTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.utils.TempFolder;
+import org.sonar.core.util.UuidFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GlobalContainerTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private GlobalContainer createContainer(List<Object> extensions) {
+ Map<String, String> props = ImmutableMap.of(CoreProperties.WORKING_DIRECTORY, temp.getRoot().getAbsolutePath(),
+ CoreProperties.GLOBAL_WORKING_DIRECTORY, temp.getRoot().getAbsolutePath());
+
+ GlobalContainer container = GlobalContainer.create(props, extensions, false);
+ container.doBeforeStart();
+ return container;
+ }
+
+ @Test
+ public void should_add_components() {
+ GlobalContainer container = createContainer(Collections.emptyList());
+
+ assertThat(container.getComponentByType(UuidFactory.class)).isNotNull();
+ assertThat(container.getComponentByType(TempFolder.class)).isNotNull();
+ }
+
+ @Test
+ public void should_add_bootstrap_extensions() {
+ GlobalContainer container = createContainer(Lists.newArrayList(Foo.class, new Bar()));
+
+ assertThat(container.getComponentByType(Foo.class)).isNotNull();
+ assertThat(container.getComponentByType(Bar.class)).isNotNull();
+ }
+
+ @BatchSide
+ public static class Foo {
+
+ }
+
+ @BatchSide
+ public static class Bar {
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalModeTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalModeTest.java
new file mode 100644
index 00000000000..5da1858e33b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalModeTest.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.CoreProperties;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GlobalModeTest {
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testModeNotSupported() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("[preview, publish, issues]");
+
+ createMode(CoreProperties.ANALYSIS_MODE, "invalid");
+ }
+
+ @Test
+ public void testOtherProperty() {
+ GlobalMode mode = createMode(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_PUBLISH);
+ assertThat(mode.isPreview()).isFalse();
+ assertThat(mode.isIssues()).isFalse();
+ assertThat(mode.isPublish()).isTrue();
+ }
+
+ @Test
+ public void testIssuesMode() {
+ GlobalMode mode = createMode(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES);
+ assertThat(mode.isPreview()).isFalse();
+ assertThat(mode.isIssues()).isTrue();
+ assertThat(mode.isPublish()).isFalse();
+ }
+
+ @Test
+ public void preview_mode_fallback_issues() {
+ GlobalMode mode = createMode(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_PREVIEW);
+
+ assertThat(mode.isIssues()).isTrue();
+ assertThat(mode.isPreview()).isFalse();
+ }
+
+ @Test
+ public void testDefault() {
+ GlobalMode mode = createMode(null, null);
+ assertThat(mode.isPreview()).isFalse();
+ assertThat(mode.isIssues()).isFalse();
+ assertThat(mode.isPublish()).isTrue();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testInvalidMode() {
+ createMode(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ANALYSIS);
+ }
+
+ private GlobalMode createMode(String key, String value) {
+ Map<String, String> map = new HashMap<>();
+ if (key != null) {
+ map.put(key, value);
+ }
+ GlobalProperties props = new GlobalProperties(map);
+ return new GlobalMode(props);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalPropertiesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalPropertiesTest.java
new file mode 100644
index 00000000000..4f3fd5cebca
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalPropertiesTest.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import com.google.common.collect.Maps;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+public class GlobalPropertiesTest {
+ @Test
+ public void test_copy_of_properties() {
+ Map<String, String> map = Maps.newHashMap();
+ map.put("foo", "bar");
+
+ GlobalProperties wrapper = new GlobalProperties(map);
+ assertThat(wrapper.properties()).containsOnly(entry("foo", "bar"));
+ assertThat(wrapper.properties()).isNotSameAs(map);
+
+ map.put("put", "after_copy");
+ assertThat(wrapper.properties()).hasSize(1);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalSettingsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalSettingsTest.java
new file mode 100644
index 00000000000..aa13b14ccb5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalSettingsTest.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.util.Collections;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class GlobalSettingsTest {
+
+ public static final String SOME_VALUE = "some_value";
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ GlobalRepositories globalRef;
+ GlobalProperties bootstrapProps;
+
+ private GlobalMode mode;
+
+ @Before
+ public void prepare() {
+ globalRef = new GlobalRepositories();
+ bootstrapProps = new GlobalProperties(Collections.<String, String>emptyMap());
+ mode = mock(GlobalMode.class);
+ }
+
+ @Test
+ public void should_load_global_settings() {
+ globalRef.globalSettings().put("sonar.cpd.cross", "true");
+
+ GlobalSettings batchSettings = new GlobalSettings(bootstrapProps, new PropertyDefinitions(), globalRef, mode);
+
+ assertThat(batchSettings.getBoolean("sonar.cpd.cross")).isTrue();
+ }
+
+ @Test
+ public void should_log_warn_msg_for_each_jdbc_property_if_present() {
+ globalRef.globalSettings().put("sonar.jdbc.url", SOME_VALUE);
+ globalRef.globalSettings().put("sonar.jdbc.username", SOME_VALUE);
+ globalRef.globalSettings().put("sonar.jdbc.password", SOME_VALUE);
+
+ new GlobalSettings(bootstrapProps, new PropertyDefinitions(), globalRef, mode);
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly(
+ "Property 'sonar.jdbc.url' is not supported any more. It will be ignored. There is no longer any DB connection to the SQ database.",
+ "Property 'sonar.jdbc.username' is not supported any more. It will be ignored. There is no longer any DB connection to the SQ database.",
+ "Property 'sonar.jdbc.password' is not supported any more. It will be ignored. There is no longer any DB connection to the SQ database."
+ );
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalTempFolderProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalTempFolderProviderTest.java
new file mode 100644
index 00000000000..e0d4ee9d3ea
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/GlobalTempFolderProviderTest.java
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.sonar.api.utils.System2;
+import org.apache.commons.io.FileUtils;
+import org.sonar.api.utils.TempFolder;
+import com.google.common.collect.ImmutableMap;
+import org.sonar.api.CoreProperties;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class GlobalTempFolderProviderTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private GlobalTempFolderProvider tempFolderProvider = new GlobalTempFolderProvider();
+
+ @Test
+ public void createTempFolderProps() throws Exception {
+ File workingDir = temp.newFolder();
+
+ TempFolder tempFolder = tempFolderProvider.provide(new GlobalProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.getAbsolutePath())));
+ tempFolder.newDir();
+ tempFolder.newFile();
+ assertThat(getCreatedTempDir(workingDir)).exists();
+ assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
+
+ FileUtils.deleteQuietly(workingDir);
+ }
+
+ @Test
+ public void cleanUpOld() throws IOException {
+ long creationTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(100);
+ File workingDir = temp.newFolder();
+
+ for (int i = 0; i < 3; i++) {
+ File tmp = new File(workingDir, ".sonartmp_" + i);
+ tmp.mkdirs();
+ setFileCreationDate(tmp, creationTime);
+ }
+
+ tempFolderProvider.provide(new GlobalProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.getAbsolutePath())));
+ // this also checks that all other temps were deleted
+ assertThat(getCreatedTempDir(workingDir)).exists();
+
+ FileUtils.deleteQuietly(workingDir);
+ }
+
+ @Test
+ public void createTempFolderSonarHome() throws Exception {
+ // with sonar home, it will be in {sonar.home}/.sonartmp
+ File sonarHome = temp.newFolder();
+ File workingDir = new File(sonarHome, CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE).getAbsoluteFile();
+
+ TempFolder tempFolder = tempFolderProvider.provide(new GlobalProperties(ImmutableMap.of("sonar.userHome", sonarHome.getAbsolutePath())));
+ tempFolder.newDir();
+ tempFolder.newFile();
+ assertThat(getCreatedTempDir(workingDir)).exists();
+ assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
+
+ FileUtils.deleteQuietly(sonarHome);
+ }
+
+ @Test
+ public void createTempFolderDefault() throws Exception {
+ System2 system = mock(System2.class);
+ tempFolderProvider = new GlobalTempFolderProvider(system);
+ File userHome = temp.newFolder();
+
+ when(system.envVariable("SONAR_USER_HOME")).thenReturn(null);
+ when(system.property("user.home")).thenReturn(userHome.getAbsolutePath().toString());
+
+ // if nothing is defined, it will be in {user.home}/.sonar/.sonartmp
+ File defaultSonarHome = new File(userHome.getAbsolutePath(), ".sonar");
+ File workingDir = new File(defaultSonarHome, CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE).getAbsoluteFile();
+ try {
+ TempFolder tempFolder = tempFolderProvider.provide(new GlobalProperties(Collections.<String, String>emptyMap()));
+ tempFolder.newDir();
+ tempFolder.newFile();
+ assertThat(getCreatedTempDir(workingDir)).exists();
+ assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
+ } finally {
+ FileUtils.deleteQuietly(workingDir);
+ }
+ }
+
+ @Test
+ public void dotWorkingDir() throws IOException {
+ File sonarHome = temp.getRoot();
+ String globalWorkDir = ".";
+ GlobalProperties globalProperties = new GlobalProperties(ImmutableMap.of("sonar.userHome", sonarHome.getAbsolutePath(),
+ CoreProperties.GLOBAL_WORKING_DIRECTORY, globalWorkDir));
+
+ TempFolder tempFolder = tempFolderProvider.provide(globalProperties);
+ File newFile = tempFolder.newFile();
+ assertThat(newFile.getParentFile().getParentFile().getAbsolutePath()).isEqualTo(sonarHome.getAbsolutePath());
+ assertThat(newFile.getParentFile().getName()).startsWith(".sonartmp_");
+ }
+
+ private File getCreatedTempDir(File workingDir) {
+ assertThat(workingDir).isDirectory();
+ assertThat(workingDir.listFiles()).hasSize(1);
+ return workingDir.listFiles()[0];
+ }
+
+ private void setFileCreationDate(File f, long time) throws IOException {
+ BasicFileAttributeView attributes = Files.getFileAttributeView(f.toPath(), BasicFileAttributeView.class);
+ FileTime creationTime = FileTime.fromMillis(time);
+ attributes.setTimes(creationTime, creationTime, creationTime);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MetricProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MetricProviderTest.java
new file mode 100644
index 00000000000..8d1ab306b8d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MetricProviderTest.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import org.junit.Test;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.measures.Metrics;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MetricProviderTest {
+ @Test
+ public void should_provide_at_least_core_metrics() {
+ MetricProvider provider = new MetricProvider();
+ List<Metric> metrics = provider.provide();
+
+ assertThat(metrics).hasSize(CoreMetrics.getMetrics().size());
+ assertThat(metrics).extracting("key").contains("ncloc");
+ }
+
+ @Test
+ public void should_provide_plugin_metrics() {
+ Metrics factory = new Metrics() {
+ public List<Metric> getMetrics() {
+ return Arrays.<Metric>asList(new Metric.Builder("custom", "Custom", Metric.ValueType.FLOAT).create());
+ }
+ };
+ MetricProvider provider = new MetricProvider(new Metrics[] {factory});
+ List<Metric> metrics = provider.provide();
+
+ assertThat(metrics.size()).isEqualTo(1 + CoreMetrics.getMetrics().size());
+ assertThat(metrics).extracting("key").contains("custom");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MockHttpServer.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MockHttpServer.java
new file mode 100644
index 00000000000..99f10de2c9e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrap/MockHttpServer.java
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrap;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.apache.commons.io.IOUtils.write;
+
+public class MockHttpServer {
+ private Server server;
+ private String responseBody;
+ private String requestBody;
+ private String mockResponseData;
+ private int mockResponseStatus = SC_OK;
+ private List<String> targets = new ArrayList<>();
+
+ public void start() throws Exception {
+ server = new Server(0);
+ server.setHandler(getMockHandler());
+ server.start();
+ }
+
+ public int getNumberRequests() {
+ return targets.size();
+ }
+
+ /**
+ * Creates an {@link org.mortbay.jetty.handler.AbstractHandler handler} returning an arbitrary String as a response.
+ *
+ * @return never <code>null</code>.
+ */
+ public Handler getMockHandler() {
+ Handler handler = new AbstractHandler() {
+
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ targets.add(target);
+ setResponseBody(getMockResponseData());
+ setRequestBody(IOUtils.toString(baseRequest.getInputStream()));
+ response.setStatus(mockResponseStatus);
+ response.setContentType("text/xml;charset=utf-8");
+ write(getResponseBody(), response.getOutputStream());
+ baseRequest.setHandled(true);
+ }
+ };
+ return handler;
+ }
+
+ public void stop() {
+ try {
+ if (server != null) {
+ server.stop();
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to stop HTTP server", e);
+ }
+ }
+
+ public String getResponseBody() {
+ return responseBody;
+ }
+
+ public void setResponseBody(String responseBody) {
+ this.responseBody = responseBody;
+ }
+
+ public String getRequestBody() {
+ return requestBody;
+ }
+
+ public void setRequestBody(String requestBody) {
+ this.requestBody = requestBody;
+ }
+
+ public void setMockResponseStatus(int status) {
+ this.mockResponseStatus = status;
+ }
+
+ public String getMockResponseData() {
+ return mockResponseData;
+ }
+
+ public void setMockResponseData(String mockResponseData) {
+ this.mockResponseData = mockResponseData;
+ }
+
+ public int getPort() {
+ return server.getConnectors()[0].getLocalPort();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/BatchTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/BatchTest.java
new file mode 100644
index 00000000000..b9696631f89
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/BatchTest.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+
+public class BatchTest {
+ @Test
+ public void testBuilder() {
+ Batch batch = newBatch();
+ assertNotNull(batch);
+
+ }
+
+ private Batch newBatch() {
+ return Batch.builder()
+ .setEnvironment(new EnvironmentInformation("Gradle", "1.0"))
+ .addComponent("fake")
+ .build();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void shouldFailIfNullComponents() {
+ Batch.builder()
+ .setEnvironment(new EnvironmentInformation("Gradle", "1.0"))
+ .setComponents(null)
+ .build();
+ }
+
+ @Test
+ public void shouldDisableLoggingConfiguration() {
+ Batch batch = Batch.builder()
+ .setEnvironment(new EnvironmentInformation("Gradle", "1.0"))
+ .addComponent("fake")
+ .setEnableLoggingConfiguration(false)
+ .build();
+ assertNull(batch.getLoggingConfiguration());
+ }
+
+ @Test
+ public void loggingConfigurationShouldBeEnabledByDefault() {
+ assertNotNull(newBatch().getLoggingConfiguration());
+ }
+
+ @Test
+ public void shoudSetLogListener() {
+ LogOutput logOutput = mock(LogOutput.class);
+ Batch batch = Batch.builder().setLogOutput(logOutput).build();
+ assertThat(batch.getLoggingConfiguration().getLogOutput()).isEqualTo(logOutput);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/EnvironmentInformationTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/EnvironmentInformationTest.java
new file mode 100644
index 00000000000..71dd0495d49
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/EnvironmentInformationTest.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EnvironmentInformationTest {
+ @Test
+ public void test_bean() {
+ EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.0");
+
+ assertThat(env.getKey()).isEqualTo("Maven Plugin");
+ assertThat(env.getVersion()).isEqualTo("2.0");
+ }
+
+ @Test
+ public void test_toString() {
+ EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.0");
+
+ assertThat(env.toString()).isEqualTo("Maven Plugin/2.0");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LogCallbackAppenderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LogCallbackAppenderTest.java
new file mode 100644
index 00000000000..6b225306fe0
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LogCallbackAppenderTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class LogCallbackAppenderTest {
+ private LogOutput listener;
+ private LogCallbackAppender appender;
+ private ILoggingEvent event;
+
+ @Before
+ public void setUp() {
+ listener = mock(LogOutput.class);
+ appender = new LogCallbackAppender(listener);
+ }
+
+ @Test
+ public void testLevelTranslation() {
+ testMessage("test", Level.INFO, LogOutput.Level.INFO);
+ testMessage("test", Level.DEBUG, LogOutput.Level.DEBUG);
+ testMessage("test", Level.ERROR, LogOutput.Level.ERROR);
+ testMessage("test", Level.TRACE, LogOutput.Level.TRACE);
+ testMessage("test", Level.WARN, LogOutput.Level.WARN);
+
+ // this should never happen
+ testMessage("test", Level.OFF, LogOutput.Level.DEBUG);
+ }
+
+ private void testMessage(String msg, Level level, LogOutput.Level translatedLevel) {
+ reset(listener);
+ event = mock(ILoggingEvent.class);
+ when(event.getFormattedMessage()).thenReturn(msg);
+ when(event.getLevel()).thenReturn(level);
+
+ appender.append(event);
+
+ verify(event).getFormattedMessage();
+ verify(event).getLevel();
+ verify(listener).log(msg, translatedLevel);
+ verifyNoMoreInteractions(event, listener);
+ }
+
+ @Test
+ public void testChangeTarget() {
+ listener = mock(LogOutput.class);
+ appender.setTarget(listener);
+ testLevelTranslation();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfigurationTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfigurationTest.java
new file mode 100644
index 00000000000..920af566e93
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfigurationTest.java
@@ -0,0 +1,167 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import com.google.common.collect.Maps;
+import java.util.Map;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class LoggingConfigurationTest {
+
+ @Test
+ public void testSetVerbose() {
+ assertThat(new LoggingConfiguration(null).setVerbose(true)
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_VERBOSE);
+
+ assertThat(new LoggingConfiguration(null).setVerbose(false)
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_DEFAULT);
+
+ assertThat(new LoggingConfiguration(null).setRootLevel("ERROR")
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo("ERROR");
+ }
+
+ @Test
+ public void testSetVerboseAnalysis() {
+ Map<String, String> globalProps = Maps.newHashMap();
+ LoggingConfiguration conf = new LoggingConfiguration(null).setProperties(globalProps);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_DEFAULT);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+
+ Map<String, String> analysisProperties = Maps.newHashMap();
+ analysisProperties.put("sonar.verbose", "true");
+
+ conf.setProperties(analysisProperties, globalProps);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_VERBOSE);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+ }
+
+ @Test
+ public void testOverrideVerbose() {
+ Map<String, String> globalProps = Maps.newHashMap();
+ globalProps.put("sonar.verbose", "true");
+ LoggingConfiguration conf = new LoggingConfiguration(null).setProperties(globalProps);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_VERBOSE);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+
+ Map<String, String> analysisProperties = Maps.newHashMap();
+ analysisProperties.put("sonar.verbose", "false");
+
+ conf.setProperties(analysisProperties, globalProps);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_DEFAULT);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+ }
+
+ @Test
+ public void shouldNotBeVerboseByDefault() {
+ assertThat(new LoggingConfiguration(null)
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_DEFAULT);
+ }
+
+ @Test
+ public void test_log_listener_setter() {
+ LogOutput listener = mock(LogOutput.class);
+ assertThat(new LoggingConfiguration(null).setLogOutput(listener).getLogOutput()).isEqualTo(listener);
+ }
+
+ @Test
+ public void test_deprecated_log_properties() {
+ Map<String, String> properties = Maps.newHashMap();
+ assertThat(new LoggingConfiguration(null).setProperties(properties)
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_DEFAULT);
+
+ properties.put("sonar.verbose", "true");
+ LoggingConfiguration conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_VERBOSE);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+
+ properties.put("sonar.verbose", "false");
+ conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo(LoggingConfiguration.LEVEL_ROOT_DEFAULT);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+
+ properties.put("sonar.verbose", "false");
+ properties.put("sonar.log.profilingLevel", "FULL");
+ conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo("DEBUG");
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("TRACE");
+
+ properties.put("sonar.verbose", "false");
+ properties.put("sonar.log.profilingLevel", "BASIC");
+ conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo("DEBUG");
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+ }
+
+ @Test
+ public void test_log_level_property() {
+ Map<String, String> properties = Maps.newHashMap();
+ LoggingConfiguration conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo("INFO");
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+
+ properties.put("sonar.log.level", "INFO");
+ conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo("INFO");
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+
+ properties.put("sonar.log.level", "DEBUG");
+ conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo("DEBUG");
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("WARN");
+
+ properties.put("sonar.log.level", "TRACE");
+ conf = new LoggingConfiguration(null).setProperties(properties);
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_ROOT_LOGGER_LEVEL)).isEqualTo("DEBUG");
+ assertThat(conf.getSubstitutionVariable(LoggingConfiguration.PROPERTY_SQL_LOGGER_LEVEL)).isEqualTo("TRACE");
+ }
+
+ @Test
+ public void testDefaultFormat() {
+ assertThat(new LoggingConfiguration(null)
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_FORMAT)).isEqualTo(LoggingConfiguration.FORMAT_DEFAULT);
+ }
+
+ @Test
+ public void testMavenFormat() {
+ assertThat(new LoggingConfiguration(new EnvironmentInformation("maven", "1.0"))
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_FORMAT)).isEqualTo(LoggingConfiguration.FORMAT_MAVEN);
+ }
+
+ @Test
+ public void testSetFormat() {
+ assertThat(new LoggingConfiguration(null).setFormat("%d %level")
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_FORMAT)).isEqualTo("%d %level");
+ }
+
+ @Test
+ public void shouldNotSetBlankFormat() {
+ assertThat(new LoggingConfiguration(null).setFormat(null)
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_FORMAT)).isEqualTo(LoggingConfiguration.FORMAT_DEFAULT);
+
+ assertThat(new LoggingConfiguration(null).setFormat("")
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_FORMAT)).isEqualTo(LoggingConfiguration.FORMAT_DEFAULT);
+
+ assertThat(new LoggingConfiguration(null).setFormat(" ")
+ .getSubstitutionVariable(LoggingConfiguration.PROPERTY_FORMAT)).isEqualTo(LoggingConfiguration.FORMAT_DEFAULT);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfiguratorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfiguratorTest.java
new file mode 100644
index 00000000000..2d719e4a657
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/bootstrapper/LoggingConfiguratorTest.java
@@ -0,0 +1,184 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.bootstrapper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LoggingConfiguratorTest {
+ private static final String DEFAULT_CLASSPATH_CONF = "/org/sonar/batch/bootstrapper/logback.xml";
+ private static final String TEST_STR = "foo";
+ private LoggingConfiguration conf = new LoggingConfiguration();
+ private ByteArrayOutputStream out;
+ private SimpleLogListener listener;
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Before
+ public void setUp() {
+ out = new ByteArrayOutputStream();
+ conf = new LoggingConfiguration();
+ listener = new SimpleLogListener();
+ }
+
+ private class SimpleLogListener implements LogOutput {
+ String msg;
+ LogOutput.Level level;
+
+ @Override
+ public void log(String msg, LogOutput.Level level) {
+ this.msg = msg;
+ this.level = level;
+ }
+ }
+
+ @Test
+ public void testWithFile() throws FileNotFoundException, IOException {
+ InputStream is = this.getClass().getResourceAsStream(DEFAULT_CLASSPATH_CONF);
+ File tmpFolder = folder.getRoot();
+ File testFile = new File(tmpFolder, "test");
+ OutputStream os = new FileOutputStream(testFile);
+ IOUtils.copy(is, os);
+ os.close();
+
+ conf.setLogOutput(listener);
+ LoggingConfigurator.apply(conf, testFile);
+
+ Logger logger = LoggerFactory.getLogger(this.getClass());
+ logger.info(TEST_STR);
+
+ assertThat(listener.msg).endsWith(TEST_STR);
+ assertThat(listener.level).isEqualTo(LogOutput.Level.INFO);
+ }
+
+ @Test
+ public void testCustomAppender() throws UnsupportedEncodingException {
+ conf.setLogOutput(listener);
+ LoggingConfigurator.apply(conf);
+
+ Logger logger = LoggerFactory.getLogger(this.getClass());
+ logger.info(TEST_STR);
+
+ assertThat(listener.msg).endsWith(TEST_STR);
+ assertThat(listener.level).isEqualTo(LogOutput.Level.INFO);
+ }
+
+ @Test
+ public void testNoStdout() throws UnsupportedEncodingException {
+ System.setOut(new PrintStream(out, false, StandardCharsets.UTF_8.name()));
+ conf.setLogOutput(listener);
+ LoggingConfigurator.apply(conf);
+
+ Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ logger.error(TEST_STR);
+ logger.info(TEST_STR);
+ logger.debug(TEST_STR);
+ assertThat(out.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void testConfigureMultipleTimes() throws UnsupportedEncodingException {
+ System.setOut(new PrintStream(out, false, StandardCharsets.UTF_8.name()));
+ conf.setLogOutput(listener);
+ LoggingConfigurator.apply(conf);
+
+ Logger logger = LoggerFactory.getLogger(this.getClass());
+ logger.debug("debug");
+ assertThat(listener.msg).isNull();
+
+ conf.setVerbose(true);
+ LoggingConfigurator.apply(conf);
+
+ logger.debug("debug");
+ assertThat(listener.msg).isEqualTo("debug");
+ }
+
+ @Test
+ public void testFormatNoEffect() throws UnsupportedEncodingException {
+ conf.setLogOutput(listener);
+ conf.setFormat("%t");
+
+ LoggingConfigurator.apply(conf);
+ Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ logger.info("info");
+
+ assertThat(listener.msg).isEqualTo("info");
+ }
+
+ @Test
+ public void testSqlClasspath() throws UnsupportedEncodingException {
+ String classpath = "/org/sonar/batch/bootstrapper/logback.xml";
+
+ conf.setLogOutput(listener);
+ conf.setShowSql(true);
+
+ LoggingConfigurator.apply(conf, classpath);
+
+ Logger logger = LoggerFactory.getLogger("java.sql");
+ logger.info("foo");
+
+ assertThat(listener.msg).endsWith(TEST_STR);
+ }
+
+ @Test
+ public void testNoListener() throws UnsupportedEncodingException {
+ System.setOut(new PrintStream(out, false, StandardCharsets.UTF_8.name()));
+ LoggingConfigurator.apply(conf);
+
+ Logger logger = LoggerFactory.getLogger(this.getClass());
+ logger.info("info");
+
+ assertThat(new String(out.toByteArray(), StandardCharsets.UTF_8)).contains("info");
+ }
+
+ @Test
+ public void testNoSqlClasspath() throws UnsupportedEncodingException {
+ String classpath = "/org/sonar/batch/bootstrapper/logback.xml";
+
+ conf.setLogOutput(listener);
+ conf.setShowSql(false);
+
+ LoggingConfigurator.apply(conf, classpath);
+
+ Logger logger = LoggerFactory.getLogger("java.sql");
+ logger.info("foo");
+
+ assertThat(listener.msg).isNull();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java
new file mode 100644
index 00000000000..c8f105de443
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+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.home.cache.PersistentCache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultProjectCacheStatusTest {
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ ProjectCacheStatus cacheStatus;
+ PersistentCache cache = mock(PersistentCache.class);
+
+ @Before
+ public void setUp() {
+ when(cache.getDirectory()).thenReturn(tmp.getRoot().toPath());
+ cacheStatus = new DefaultProjectCacheStatus(cache);
+ }
+
+ @Test
+ public void errorSave() throws IOException {
+ when(cache.getDirectory()).thenReturn(tmp.getRoot().toPath().resolve("unexistent_folder"));
+ cacheStatus = new DefaultProjectCacheStatus(cache);
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Failed to write cache sync status");
+ cacheStatus.save();
+ }
+
+ @Test
+ public void errorStatus() throws IOException {
+ Files.write("trash".getBytes(StandardCharsets.UTF_8), new File(tmp.getRoot(), "cache-sync-status"));
+ cacheStatus = new DefaultProjectCacheStatus(cache);
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Failed to read cache sync status");
+ cacheStatus.getSyncStatus();
+ }
+
+ @Test
+ public void testSave() {
+ cacheStatus.save();
+ assertThat(cacheStatus.getSyncStatus()).isNotNull();
+ assertThat(age(cacheStatus.getSyncStatus())).isLessThan(2000);
+ }
+
+ @Test
+ public void testDelete() {
+ cacheStatus.save();
+ cacheStatus.delete();
+ assertThat(cacheStatus.getSyncStatus()).isNull();
+ }
+
+ private long age(Date date) {
+ return (new Date().getTime()) - date.getTime();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/GlobalPersistentCacheProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/GlobalPersistentCacheProviderTest.java
new file mode 100644
index 00000000000..3f019caae02
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/GlobalPersistentCacheProviderTest.java
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import org.sonar.home.cache.PersistentCache;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+
+import static org.junit.Assert.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+public class GlobalPersistentCacheProviderTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private GlobalPersistentCacheProvider provider;
+ private GlobalProperties globalProperties;
+
+ @Before
+ public void setUp() {
+ HashMap<String, String> map = new HashMap<>();
+ map.put("sonar.userHome", temp.getRoot().getAbsolutePath());
+ globalProperties = new GlobalProperties(map);
+ provider = new GlobalPersistentCacheProvider();
+ }
+
+ @Test
+ public void test_path() {
+ PersistentCache cache = provider.provide(globalProperties);
+ assertThat(cache.getDirectory()).isEqualTo(temp.getRoot().toPath()
+ .resolve("ws_cache")
+ .resolve("http%3A%2F%2Flocalhost%3A9000")
+ .resolve("global"));
+ }
+
+ @Test
+ public void test_singleton() {
+ assertTrue(provider.provide(globalProperties) == provider.provide(globalProperties));
+ }
+
+ @Test
+ public void test_without_sonar_home() {
+ globalProperties = new GlobalProperties(new HashMap<String, String>());
+ PersistentCache cache = provider.provide(globalProperties);
+ assertThat(cache.getDirectory().toAbsolutePath().toString()).startsWith(findHome().toAbsolutePath().toString());
+
+ }
+
+ private static Path findHome() {
+ String home = System.getenv("SONAR_USER_HOME");
+
+ if (home != null) {
+ return Paths.get(home);
+ }
+
+ home = System.getProperty("user.home");
+ return Paths.get(home, ".sonar");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java
new file mode 100644
index 00000000000..c06bb94ca08
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Date;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.batch.repository.QualityProfileLoader;
+import org.sonar.batch.rule.ActiveRulesLoader;
+import org.sonar.batch.rule.LoadedActiveRule;
+import org.sonar.batch.rule.RulesLoader;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class NonAssociatedCacheSynchronizerTest {
+ private NonAssociatedCacheSynchronizer synchronizer;
+
+ @Mock
+ private RulesLoader rulesLoader;
+ @Mock
+ private QualityProfileLoader qualityProfileLoader;
+ @Mock
+ private ActiveRulesLoader activeRulesLoader;
+ @Mock
+ private ProjectCacheStatus cacheStatus;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ QualityProfile pf = QualityProfile.newBuilder().setKey("profile").setName("profile").setLanguage("lang").build();
+ LoadedActiveRule ar = new LoadedActiveRule();
+
+ when(qualityProfileLoader.loadDefault(null, null)).thenReturn(ImmutableList.of(pf));
+ when(activeRulesLoader.load("profile", null)).thenReturn(ImmutableList.of(ar));
+
+ synchronizer = new NonAssociatedCacheSynchronizer(rulesLoader, qualityProfileLoader, activeRulesLoader, cacheStatus);
+ }
+
+ @Test
+ public void dont_sync_if_exists() {
+ when(cacheStatus.getSyncStatus()).thenReturn(new Date());
+ synchronizer.execute(false);
+ verifyZeroInteractions(rulesLoader, qualityProfileLoader, activeRulesLoader);
+ }
+
+ @Test
+ public void always_sync_if_force() {
+ when(cacheStatus.getSyncStatus()).thenReturn(new Date());
+ synchronizer.execute(true);
+ checkSync();
+ }
+
+ @Test
+ public void sync_if_doesnt_exist() {
+ synchronizer.execute(false);
+ checkSync();
+ }
+
+ private void checkSync() {
+ verify(cacheStatus).getSyncStatus();
+ verify(cacheStatus).save();
+ verify(rulesLoader).load(null);
+ verify(qualityProfileLoader).loadDefault(null, null);
+ verify(activeRulesLoader).load("profile", null);
+
+ verifyNoMoreInteractions(qualityProfileLoader, activeRulesLoader);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java
new file mode 100644
index 00000000000..52df42e035d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.batch.analysis.AnalysisProperties;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.repository.DefaultProjectRepositoriesLoader;
+import org.sonar.batch.repository.DefaultQualityProfileLoader;
+import org.sonar.batch.repository.DefaultServerIssuesLoader;
+import org.sonar.batch.repository.ProjectRepositories;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
+import org.sonar.batch.repository.QualityProfileLoader;
+import org.sonar.batch.repository.ServerIssuesLoader;
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.batch.rule.ActiveRulesLoader;
+import org.sonar.batch.rule.DefaultActiveRulesLoader;
+import org.sonar.batch.rule.LoadedActiveRule;
+import org.sonar.batch.rule.RulesLoader;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+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 ProjectCacheSynchronizerTest {
+ private static final String PROJECT_KEY = "org.codehaus.sonar-plugins:sonar-scm-git-plugin";
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Mock
+ private ProjectDefinition project;
+ @Mock
+ private ProjectCacheStatus cacheStatus;
+ @Mock
+ private DefaultAnalysisMode analysisMode;
+ @Mock
+ private AnalysisProperties properties;
+ @Mock
+ private RulesLoader rulesLoader;
+
+ private ServerIssuesLoader issuesLoader;
+ private UserRepositoryLoader userRepositoryLoader;
+ private QualityProfileLoader qualityProfileLoader;
+ private ActiveRulesLoader activeRulesLoader;
+ private ProjectRepositoriesLoader projectRepositoriesLoader;
+
+ @Before
+ public void setUp() throws IOException {
+ MockitoAnnotations.initMocks(this);
+
+ when(analysisMode.isIssues()).thenReturn(true);
+ when(properties.properties()).thenReturn(new HashMap<String, String>());
+ }
+
+ private ProjectCacheSynchronizer createMockedLoaders(boolean projectExists, Date lastAnalysisDate) {
+ issuesLoader = mock(DefaultServerIssuesLoader.class);
+ userRepositoryLoader = mock(UserRepositoryLoader.class);
+ qualityProfileLoader = mock(DefaultQualityProfileLoader.class);
+ activeRulesLoader = mock(DefaultActiveRulesLoader.class);
+ projectRepositoriesLoader = mock(DefaultProjectRepositoriesLoader.class);
+
+ QualityProfile pf = QualityProfile.newBuilder().setKey("profile").setName("profile").setLanguage("lang").build();
+ LoadedActiveRule ar = new LoadedActiveRule();
+ ProjectRepositories repo = mock(ProjectRepositories.class);
+
+ when(qualityProfileLoader.load(PROJECT_KEY, null, null)).thenReturn(ImmutableList.of(pf));
+ when(qualityProfileLoader.loadDefault(null, null)).thenReturn(ImmutableList.of(pf));
+ when(activeRulesLoader.load("profile", null)).thenReturn(ImmutableList.of(ar));
+ when(repo.lastAnalysisDate()).thenReturn(lastAnalysisDate);
+ when(repo.exists()).thenReturn(projectExists);
+ when(projectRepositoriesLoader.load(anyString(), anyBoolean(), any(MutableBoolean.class))).thenReturn(repo);
+
+ return new ProjectCacheSynchronizer(rulesLoader, qualityProfileLoader, projectRepositoriesLoader, activeRulesLoader, issuesLoader, userRepositoryLoader, cacheStatus);
+ }
+
+ @Test
+ public void testLoadersUsage() {
+ ProjectCacheSynchronizer synchronizer = createMockedLoaders(true, new Date());
+ synchronizer.load(PROJECT_KEY, false);
+
+ verify(issuesLoader).load(eq(PROJECT_KEY), any(Function.class));
+ verify(rulesLoader).load(null);
+ verify(qualityProfileLoader).load(PROJECT_KEY, null, null);
+ verify(activeRulesLoader).load("profile", null);
+ verify(projectRepositoriesLoader).load(eq(PROJECT_KEY), eq(true), any(MutableBoolean.class));
+
+ verifyNoMoreInteractions(issuesLoader, userRepositoryLoader, qualityProfileLoader, activeRulesLoader, projectRepositoriesLoader);
+ }
+
+ @Test
+ public void testLoadersUsage_NoLastAnalysis() {
+ ProjectCacheSynchronizer synchronizer = createMockedLoaders(true, null);
+ synchronizer.load(PROJECT_KEY, false);
+
+ verify(projectRepositoriesLoader).load(eq(PROJECT_KEY), eq(true), any(MutableBoolean.class));
+ verify(qualityProfileLoader).load(PROJECT_KEY, null, null);
+ verify(activeRulesLoader).load("profile", null);
+
+ verifyNoMoreInteractions(issuesLoader, userRepositoryLoader, qualityProfileLoader, activeRulesLoader, projectRepositoriesLoader);
+ }
+
+ @Test
+ public void testLoadersUsage_ProjectDoesntExist() {
+ ProjectCacheSynchronizer synchronizer = createMockedLoaders(false, null);
+ synchronizer.load(PROJECT_KEY, false);
+
+ verify(projectRepositoriesLoader).load(eq(PROJECT_KEY), eq(true), any(MutableBoolean.class));
+ verify(qualityProfileLoader).loadDefault(null, null);
+ verify(activeRulesLoader).load("profile", null);
+
+ verifyNoMoreInteractions(issuesLoader, userRepositoryLoader, qualityProfileLoader, activeRulesLoader, projectRepositoriesLoader);
+ }
+
+ @Test
+ public void testLastAnalysisToday() {
+ ProjectCacheSynchronizer synchronizer = createMockedLoaders(true, new Date());
+
+ when(cacheStatus.getSyncStatus()).thenReturn(new Date());
+ synchronizer.load(PROJECT_KEY, false);
+
+ verify(cacheStatus).getSyncStatus();
+ verifyNoMoreInteractions(issuesLoader, userRepositoryLoader, qualityProfileLoader, activeRulesLoader, projectRepositoriesLoader, cacheStatus);
+ }
+
+ @Test
+ public void testLastAnalysisYesterday() {
+ ProjectCacheSynchronizer synchronizer = createMockedLoaders(true, new Date());
+
+ Date d = new Date(new Date().getTime() - 60 * 60 * 24 * 1000);
+ when(cacheStatus.getSyncStatus()).thenReturn(d);
+ synchronizer.load(PROJECT_KEY, false);
+
+ verify(cacheStatus).save();
+ verify(cacheStatus).getSyncStatus();
+ }
+
+ @Test
+ public void testDontFailOnError() {
+ ProjectCacheSynchronizer synchronizer = createMockedLoaders(true, new Date());
+
+ Date d = new Date(new Date().getTime() - 60 * 60 * 24 * 1000);
+ when(cacheStatus.getSyncStatus()).thenReturn(d);
+
+ when(projectRepositoriesLoader.load(anyString(), anyBoolean(), any(MutableBoolean.class))).thenThrow(IllegalStateException.class);
+ synchronizer.load(PROJECT_KEY, false);
+
+ verify(cacheStatus).getSyncStatus();
+ verifyNoMoreInteractions(cacheStatus);
+ }
+
+ @Test
+ public void testForce() {
+ ProjectCacheSynchronizer synchronizer = createMockedLoaders(true, new Date());
+
+ when(cacheStatus.getSyncStatus()).thenReturn(new Date());
+ synchronizer.load(PROJECT_KEY, true);
+
+ verify(cacheStatus).save();
+ verify(cacheStatus).getSyncStatus();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java
new file mode 100644
index 00000000000..69c142556ae
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import org.sonar.api.batch.bootstrap.ProjectKey;
+
+import org.sonar.batch.util.BatchUtils;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.cache.ProjectPersistentCacheProvider;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Collections;
+
+import static org.mockito.Mockito.mock;
+import org.junit.Before;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Test;
+
+public class ProjectPersistentCacheProviderTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private ProjectPersistentCacheProvider provider = null;
+ private GlobalProperties props = null;
+ private DefaultAnalysisMode mode = null;
+ private ProjectKey key = null;
+
+ @Before
+ public void prepare() {
+ key = new ProjectKeySupplier("proj");
+ props = new GlobalProperties(Collections.<String, String>emptyMap());
+ mode = mock(DefaultAnalysisMode.class);
+ provider = new ProjectPersistentCacheProvider();
+ }
+
+ @Test
+ public void test_singleton() {
+ assertThat(provider.provide(props, mode, key)).isEqualTo(provider.provide(props, mode, key));
+ }
+
+ @Test
+ public void test_cache_dir() {
+ assertThat(provider.provide(props, mode, key).getDirectory().toFile()).exists().isDirectory();
+ }
+
+ @Test
+ public void test_home() {
+ File f = temp.getRoot();
+ props.properties().put("sonar.userHome", f.getAbsolutePath());
+ Path expected = f.toPath()
+ .resolve("ws_cache")
+ .resolve("http%3A%2F%2Flocalhost%3A9000")
+ .resolve( BatchUtils.getServerVersion())
+ .resolve("projects")
+ .resolve("proj");
+
+ assertThat(provider.provide(props, mode, key).getDirectory()).isEqualTo(expected);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectSyncContainerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectSyncContainerTest.java
new file mode 100644
index 00000000000..948f888cd68
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/ProjectSyncContainerTest.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import java.util.HashMap;
+import org.junit.Test;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.core.platform.ComponentContainer;
+import org.sonar.home.cache.PersistentCache;
+import org.sonar.scanner.protocol.input.ProjectRepositories;
+import org.sonarqube.ws.client.WsClient;
+
+import static org.mockito.Mockito.mock;
+
+public class ProjectSyncContainerTest {
+ private ComponentContainer createParentContainer() {
+ PersistentCache cache = mock(PersistentCache.class);
+ WsClient server = mock(WsClient.class);
+
+ GlobalProperties globalProps = new GlobalProperties(new HashMap<String, String>());
+ ComponentContainer parent = new ComponentContainer();
+ parent.add(cache);
+ parent.add(server);
+ parent.add(globalProps);
+ return parent;
+ }
+
+ @Test
+ public void testProjectRepository() {
+ ProjectSyncContainer container = new ProjectSyncContainer(createParentContainer(), "my:project", true);
+ container.doBeforeStart();
+ container.getPicoContainer().start();
+ container.getComponentByType(ProjectRepositories.class);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java
new file mode 100644
index 00000000000..ce9d88a037c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.home.cache.PersistentCache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class StrategyWSLoaderProviderTest {
+ @Mock
+ private PersistentCache cache;
+
+ @Mock
+ private BatchWsClient client;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testStrategy() {
+ StrategyWSLoaderProvider provider = new StrategyWSLoaderProvider(LoadStrategy.CACHE_FIRST);
+ WSLoader wsLoader = provider.provide(cache, client);
+
+ assertThat(wsLoader.getDefaultStrategy()).isEqualTo(LoadStrategy.CACHE_FIRST);
+ }
+
+ @Test
+ public void testSingleton() {
+ StrategyWSLoaderProvider provider = new StrategyWSLoaderProvider(LoadStrategy.CACHE_FIRST);
+ WSLoader wsLoader = provider.provide(cache, client);
+
+ assertThat(provider.provide(null, null)).isEqualTo(wsLoader);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/WSLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/WSLoaderTest.java
new file mode 100644
index 00000000000..ad7bb763d67
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cache/WSLoaderTest.java
@@ -0,0 +1,264 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.commons.io.IOUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.home.cache.PersistentCache;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.MockWsResponse;
+import org.sonarqube.ws.client.WsRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class WSLoaderTest {
+ private final static String ID = "dummy";
+ private final static String cacheValue = "cache";
+ private final static String serverValue = "server";
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ BatchWsClient ws = mock(BatchWsClient.class, Mockito.RETURNS_DEEP_STUBS);
+ PersistentCache cache = mock(PersistentCache.class);
+
+ @Test
+ public void dont_retry_server_offline() throws IOException {
+ turnServerOffline();
+ when(cache.getString(ID)).thenReturn(cacheValue);
+ WSLoader underTest = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+
+ assertResult(underTest.loadString(ID), cacheValue, true);
+ assertResult(underTest.loadString(ID), cacheValue, true);
+
+ assertUsedServer(1);
+ assertUsedCache(2);
+ }
+
+ @Test
+ public void get_stream_from_cache() throws IOException {
+ InputStream is = IOUtils.toInputStream("is");
+ when(cache.getStream(ID)).thenReturn(is);
+
+ WSLoader loader = new WSLoader(LoadStrategy.CACHE_FIRST, cache, ws);
+ WSLoaderResult<InputStream> result = loader.loadStream(ID);
+
+ assertThat(result.get()).isEqualTo(is);
+ verify(cache).getStream(ID);
+ verifyNoMoreInteractions(cache, ws);
+ }
+
+ @Test
+ public void put_stream_in_cache() throws IOException {
+ InputStream input = IOUtils.toInputStream("is");
+
+ when(ws.call(any(WsRequest.class))).thenReturn(new MockWsResponse().setContent(input));
+ when(cache.getStream(ID)).thenReturn(input);
+
+ // SERVER_FIRST -> load from server then put to cache
+ WSLoader underTest = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+ WSLoaderResult<InputStream> result = underTest.loadStream(ID);
+ assertThat(result.get()).isEqualTo(input);
+
+ InOrder inOrder = inOrder(ws, cache);
+ inOrder.verify(ws).call(any(WsRequest.class));
+ inOrder.verify(cache).put(eq(ID), any(InputStream.class));
+ inOrder.verify(cache).getStream(ID);
+ verifyNoMoreInteractions(cache, ws);
+ }
+
+ @Test
+ public void test_cache_strategy_fallback() throws IOException {
+ turnCacheEmpty();
+ when(ws.call(any(WsRequest.class))).thenReturn(new MockWsResponse().setContent(serverValue));
+ WSLoader loader = new WSLoader(LoadStrategy.CACHE_FIRST, cache, ws);
+
+ assertResult(loader.loadString(ID), serverValue, false);
+
+ InOrder inOrder = inOrder(ws, cache);
+ inOrder.verify(cache).getString(ID);
+ inOrder.verify(ws).call(any(WsRequest.class));
+ }
+
+ @Test
+ public void test_server_strategy_fallback() throws IOException {
+ turnServerOffline();
+ when(cache.getString(ID)).thenReturn(cacheValue);
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+
+ assertResult(loader.loadString(ID), cacheValue, true);
+
+ InOrder inOrder = inOrder(ws, cache);
+ inOrder.verify(ws).call(any(WsRequest.class));
+ inOrder.verify(cache).getString(ID);
+ }
+
+ @Test
+ public void test_put_cache() throws IOException {
+ when(ws.call(any(WsRequest.class))).thenReturn(new MockWsResponse().setContent(serverValue));
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+ loader.loadString(ID);
+ verify(cache).put(ID, serverValue.getBytes());
+ }
+
+ @Test
+ public void test_throw_cache_exception_fallback() throws IOException {
+ turnServerOffline();
+
+ when(cache.getString(ID)).thenThrow(new NullPointerException());
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+
+ try {
+ loader.loadString(ID);
+ fail("NPE expected");
+ } catch (NullPointerException e) {
+ assertUsedServer(1);
+ assertUsedCache(1);
+ }
+ }
+
+ @Test
+ public void test_throw_cache_exception() throws IOException {
+ when(cache.getString(ID)).thenThrow(new IllegalStateException());
+
+ WSLoader loader = new WSLoader(LoadStrategy.CACHE_FIRST, cache, ws);
+
+ try {
+ loader.loadString(ID);
+ fail("IllegalStateException expected");
+ } catch (IllegalStateException e) {
+ assertUsedServer(0);
+ assertUsedCache(1);
+ }
+ }
+
+ @Test
+ public void test_throw_http_exceptions() {
+ when(ws.call(any(WsRequest.class))).thenThrow(new HttpException("url", 500));
+
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+
+ try {
+ loader.loadString(ID);
+ fail("IllegalStateException expected");
+ } catch (HttpException e) {
+ // cache should not be used
+ verifyNoMoreInteractions(cache);
+ }
+ }
+
+ @Test
+ public void test_server_only_not_available() {
+ turnServerOffline();
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Server is not available");
+
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_ONLY, cache, ws);
+ loader.loadString(ID);
+ }
+
+ @Test
+ public void test_server_cache_not_available() throws IOException {
+ turnServerOffline();
+ turnCacheEmpty();
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Server is not accessible and data is not cached");
+
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+ loader.loadString(ID);
+ }
+
+ @Test
+ public void test_cache_only_available() throws IOException {
+ turnCacheEmpty();
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Data is not cached");
+
+ WSLoader loader = new WSLoader(LoadStrategy.CACHE_ONLY, cache, ws);
+ loader.loadString(ID);
+ }
+
+ @Test
+ public void test_server_strategy() throws IOException {
+ when(ws.call(any(WsRequest.class))).thenReturn(new MockWsResponse().setContent(serverValue));
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+ assertResult(loader.loadString(ID), serverValue, false);
+
+ // should not fetch from cache
+ verify(cache).put(ID, serverValue.getBytes());
+ verifyNoMoreInteractions(cache);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void test_server_only() throws IOException {
+ turnServerOffline();
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_ONLY, cache, ws);
+ loader.loadString(ID);
+ }
+
+ @Test
+ public void test_string() {
+ when(ws.call(any(WsRequest.class))).thenReturn(new MockWsResponse().setContent(serverValue));
+ WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, ws);
+ assertResult(loader.loadString(ID), serverValue, false);
+ }
+
+ private void assertUsedCache(int times) throws IOException {
+ verify(cache, times(times)).getString(ID);
+ }
+
+ private void assertUsedServer(int times) {
+ verify(ws, times(times)).call(any(WsRequest.class));
+ }
+
+ private void assertResult(WSLoaderResult<String> result, String expected, boolean fromCache) {
+ assertThat(result).isNotNull();
+ assertThat(result.get()).isEqualTo(expected);
+ assertThat(result.isFromCache()).isEqualTo(fromCache);
+ }
+
+ private void turnServerOffline() {
+ when(ws.call(any(WsRequest.class))).thenThrow(new IllegalStateException());
+ }
+
+ private void turnCacheEmpty() throws IOException {
+ when(cache.getString(ID)).thenReturn(null);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java
new file mode 100644
index 00000000000..814e7d69d38
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CpdComponentsTest {
+
+ @Test
+ public void getExtensions() {
+ assertThat(CpdComponents.all().size()).isGreaterThan(0);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdExecutorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdExecutorTest.java
new file mode 100644
index 00000000000..6a6c209348f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdExecutorTest.java
@@ -0,0 +1,239 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.duplications.index.CloneGroup;
+import org.sonar.duplications.index.ClonePart;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.protocol.output.ScannerReport.Duplicate;
+import org.sonar.scanner.protocol.output.ScannerReport.Duplication;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class CpdExecutorTest {
+ private CpdExecutor executor;
+ private Settings settings;
+ private SonarCpdBlockIndex index;
+ private ReportPublisher publisher;
+ private BatchComponentCache componentCache;
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ // private AbstractCpdEngine engine;
+
+ private ScannerReportReader reader;
+ private BatchComponent batchComponent1;
+ private BatchComponent batchComponent2;
+ private BatchComponent batchComponent3;
+
+ @Before
+ public void setUp() throws IOException {
+ File outputDir = temp.newFolder();
+
+ settings = new Settings();
+ index = mock(SonarCpdBlockIndex.class);
+ publisher = mock(ReportPublisher.class);
+ when(publisher.getWriter()).thenReturn(new ScannerReportWriter(outputDir));
+ componentCache = new BatchComponentCache();
+ executor = new CpdExecutor(settings, index, publisher, componentCache);
+ reader = new ScannerReportReader(outputDir);
+
+ Project p = new Project("foo");
+ componentCache.add(p, null).setInputComponent(new DefaultInputModule("foo"));
+
+ batchComponent1 = createComponent("src/Foo.php", 5);
+ batchComponent2 = createComponent("src/Foo2.php", 5);
+ batchComponent3 = createComponent("src/Foo3.php", 5);
+ }
+
+ private BatchComponent createComponent(String relativePath, int lines) {
+ org.sonar.api.resources.Resource sampleFile = org.sonar.api.resources.File.create("relativePath").setEffectiveKey("foo:" + relativePath);
+ return componentCache.add(sampleFile, null).setInputComponent(new DefaultInputFile("foo", relativePath).setLines(lines));
+ }
+
+ @Test
+ public void defaultMinimumTokens() {
+ assertThat(executor.getMinimumTokens("java")).isEqualTo(100);
+ }
+
+ @Test
+ public void minimumTokensByLanguage() {
+ settings.setProperty("sonar.cpd.java.minimumTokens", "42");
+ settings.setProperty("sonar.cpd.php.minimumTokens", "33");
+ assertThat(executor.getMinimumTokens("java")).isEqualTo(42);
+
+ settings.setProperty("sonar.cpd.java.minimumTokens", "42");
+ settings.setProperty("sonar.cpd.php.minimumTokens", "33");
+ assertThat(executor.getMinimumTokens("php")).isEqualTo(33);
+ }
+
+ @Test
+ public void testNothingToSave() {
+ executor.saveDuplications(batchComponent1, Collections.<CloneGroup>emptyList());
+ assertThat(reader.readComponentDuplications(batchComponent1.batchId())).hasSize(0);
+ }
+
+ @Test
+ public void reportOneSimpleDuplicationBetweenTwoFiles() {
+ List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart(batchComponent1.key(), 0, 2, 4), new ClonePart(batchComponent2.key(), 0, 15, 17)));
+
+ executor.saveDuplications(batchComponent1, groups);
+
+ Duplication[] dups = readDuplications(1);
+ assertDuplication(dups[0], 2, 4, batchComponent2.batchId(), 15, 17);
+ }
+
+ @Test
+ public void reportDuplicationOnSameFile() throws Exception {
+ List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart(batchComponent1.key(), 0, 5, 204), new ClonePart(batchComponent1.key(), 0, 215, 414)));
+ executor.saveDuplications(batchComponent1, groups);
+
+ Duplication[] dups = readDuplications(1);
+ assertDuplication(dups[0], 5, 204, null, 215, 414);
+ }
+
+ @Test
+ public void reportTooManyDuplicates() throws Exception {
+ // 1 origin part + 101 duplicates = 102
+ List<ClonePart> parts = new ArrayList<>(CpdExecutor.MAX_CLONE_PART_PER_GROUP + 2);
+ for (int i = 0; i < CpdExecutor.MAX_CLONE_PART_PER_GROUP + 2; i++) {
+ parts.add(new ClonePart(batchComponent1.key(), i, i, i + 1));
+ }
+ List<CloneGroup> groups = Arrays.asList(CloneGroup.builder().setLength(0).setOrigin(parts.get(0)).setParts(parts).build());
+ executor.saveDuplications(batchComponent1, groups);
+
+ Duplication[] dups = readDuplications(1);
+ assertThat(dups[0].getDuplicateList()).hasSize(CpdExecutor.MAX_CLONE_PART_PER_GROUP);
+
+ assertThat(logTester.logs(LoggerLevel.WARN))
+ .contains("Too many duplication references on file " + batchComponent1.inputComponent() + " for block at line 0. Keep only the first "
+ + CpdExecutor.MAX_CLONE_PART_PER_GROUP + " references.");
+ }
+
+ @Test
+ public void reportTooManyDuplications() throws Exception {
+ // 1 origin part + 101 duplicates = 102
+ List<CloneGroup> dups = new ArrayList<>(CpdExecutor.MAX_CLONE_GROUP_PER_FILE + 1);
+ for (int i = 0; i < CpdExecutor.MAX_CLONE_GROUP_PER_FILE + 1; i++) {
+ ClonePart clonePart = new ClonePart(batchComponent1.key(), i, i, i + 1);
+ ClonePart dupPart = new ClonePart(batchComponent1.key(), i + 1, i + 1, i + 2);
+ dups.add(newCloneGroup(clonePart, dupPart));
+ }
+ executor.saveDuplications(batchComponent1, dups);
+
+ assertThat(reader.readComponentDuplications(batchComponent1.batchId())).hasSize(CpdExecutor.MAX_CLONE_GROUP_PER_FILE);
+
+ assertThat(logTester.logs(LoggerLevel.WARN))
+ .contains("Too many duplication groups on file " + batchComponent1.inputComponent() + ". Keep only the first " + CpdExecutor.MAX_CLONE_GROUP_PER_FILE + " groups.");
+ }
+
+ @Test
+ public void reportOneDuplicatedGroupInvolvingMoreThanTwoFiles() throws Exception {
+ List<CloneGroup> groups = Arrays
+ .asList(newCloneGroup(new ClonePart(batchComponent1.key(), 0, 5, 204), new ClonePart(batchComponent2.key(), 0, 15, 214), new ClonePart(batchComponent3.key(), 0, 25, 224)));
+ executor.saveDuplications(batchComponent1, groups);
+
+ Duplication[] dups = readDuplications(1);
+ assertDuplication(dups[0], 5, 204, 2);
+ assertDuplicate(dups[0].getDuplicate(0), batchComponent2.batchId(), 15, 214);
+ assertDuplicate(dups[0].getDuplicate(1), batchComponent3.batchId(), 25, 224);
+ }
+
+ @Test
+ public void reportTwoDuplicatedGroupsInvolvingThreeFiles() throws Exception {
+ List<CloneGroup> groups = Arrays.asList(
+ newCloneGroup(new ClonePart(batchComponent1.key(), 0, 5, 204), new ClonePart(batchComponent2.key(), 0, 15, 214)),
+ newCloneGroup(new ClonePart(batchComponent1.key(), 0, 15, 214), new ClonePart(batchComponent3.key(), 0, 15, 214)));
+ executor.saveDuplications(batchComponent1, groups);
+
+ Duplication[] dups = readDuplications(2);
+ assertDuplication(dups[0], 5, 204, batchComponent2.batchId(), 15, 214);
+ assertDuplication(dups[1], 15, 214, batchComponent3.batchId(), 15, 214);
+ }
+
+ private Duplication[] readDuplications(int expected) {
+ assertThat(reader.readComponentDuplications(batchComponent1.batchId())).hasSize(expected);
+ Duplication[] duplications = new Duplication[expected];
+ CloseableIterator<Duplication> dups = reader.readComponentDuplications(batchComponent1.batchId());
+
+ for(int i = 0; i< expected; i++) {
+ duplications[i] = dups.next();
+ }
+ dups.close();
+ return duplications;
+ }
+
+ private void assertDuplicate(Duplicate d, int otherFileRef, int rangeStartLine, int rangeEndLine) {
+ assertThat(d.getOtherFileRef()).isEqualTo(otherFileRef);
+ assertThat(d.getRange().getStartLine()).isEqualTo(rangeStartLine);
+ assertThat(d.getRange().getEndLine()).isEqualTo(rangeEndLine);
+ }
+
+ private void assertDuplication(Duplication d, int originStartLine, int originEndLine, int numDuplicates) {
+ assertThat(d.getOriginPosition().getStartLine()).isEqualTo(originStartLine);
+ assertThat(d.getOriginPosition().getEndLine()).isEqualTo(originEndLine);
+ assertThat(d.getDuplicateList()).hasSize(numDuplicates);
+ }
+
+ private void assertDuplication(Duplication d, int originStartLine, int originEndLine, Integer otherFileRef, int rangeStartLine, int rangeEndLine) {
+ assertThat(d.getOriginPosition().getStartLine()).isEqualTo(originStartLine);
+ assertThat(d.getOriginPosition().getEndLine()).isEqualTo(originEndLine);
+ assertThat(d.getDuplicateList()).hasSize(1);
+ if(otherFileRef != null) {
+ assertThat(d.getDuplicate(0).getOtherFileRef()).isEqualTo(otherFileRef);
+ } else {
+ assertThat(d.getDuplicate(0).hasOtherFileRef()).isFalse();
+ }
+ assertThat(d.getDuplicate(0).getRange().getStartLine()).isEqualTo(rangeStartLine);
+ assertThat(d.getDuplicate(0).getRange().getEndLine()).isEqualTo(rangeEndLine);
+ }
+
+ private CloneGroup newCloneGroup(ClonePart... parts) {
+ return CloneGroup.builder().setLength(0).setOrigin(parts[0]).setParts(Arrays.asList(parts)).build();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java
new file mode 100644
index 00000000000..ce27f0776c4
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Java;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CpdSensorTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ JavaCpdBlockIndexer sonarEngine;
+ DefaultCpdBlockIndexer sonarBridgeEngine;
+ CpdSensor sensor;
+ Settings settings;
+
+ @Before
+ public void setUp() throws IOException {
+ sonarEngine = new JavaCpdBlockIndexer(null, null, null);
+ sonarBridgeEngine = new DefaultCpdBlockIndexer(new CpdMappings(), null, null, null);
+ settings = new Settings(new PropertyDefinitions(CpdComponents.class));
+
+ DefaultFileSystem fs = new DefaultFileSystem(temp.newFolder().toPath());
+ sensor = new CpdSensor(sonarEngine, sonarBridgeEngine, settings, fs);
+ }
+
+ @Test
+ public void test_global_skip() {
+ settings.setProperty("sonar.cpd.skip", true);
+ assertThat(sensor.isSkipped(Java.KEY)).isTrue();
+ }
+
+ @Test
+ public void should_not_skip_by_default() {
+ assertThat(sensor.isSkipped(Java.KEY)).isFalse();
+ }
+
+ @Test
+ public void should_skip_by_language() {
+ settings.setProperty("sonar.cpd.skip", false);
+ settings.setProperty("sonar.cpd.php.skip", true);
+
+ assertThat(sensor.isSkipped("php")).isTrue();
+ assertThat(sensor.isSkipped(Java.KEY)).isFalse();
+ }
+
+ @Test
+ public void test_engine() {
+ assertThat(sensor.getEngine(Java.KEY)).isSameAs(sonarEngine);
+ assertThat(sensor.getEngine("PHP")).isSameAs(sonarBridgeEngine);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DefaultCpdBlockIndexerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DefaultCpdBlockIndexerTest.java
new file mode 100644
index 00000000000..9fcd03ac940
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DefaultCpdBlockIndexerTest.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.sonar.api.config.Settings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class DefaultCpdBlockIndexerTest {
+
+ private DefaultCpdBlockIndexer engine;
+ private Settings settings;
+
+ @Before
+ public void init() {
+ settings = new Settings();
+ engine = new DefaultCpdBlockIndexer(null, null, settings, null);
+ }
+
+ @Test
+ public void shouldLogExclusions() {
+ Logger logger = mock(Logger.class);
+ engine.logExclusions(new String[0], logger);
+ verify(logger, never()).info(anyString());
+
+ logger = mock(Logger.class);
+ engine.logExclusions(new String[] {"Foo*", "**/Bar*"}, logger);
+
+ String message = "Copy-paste detection exclusions:"
+ + "\n Foo*"
+ + "\n **/Bar*";
+ verify(logger, times(1)).info(message);
+ }
+
+ @Test
+ public void shouldReturnDefaultBlockSize() {
+ assertThat(DefaultCpdBlockIndexer.getDefaultBlockSize("cobol")).isEqualTo(30);
+ assertThat(DefaultCpdBlockIndexer.getDefaultBlockSize("abap")).isEqualTo(20);
+ assertThat(DefaultCpdBlockIndexer.getDefaultBlockSize("other")).isEqualTo(10);
+ }
+
+ @Test
+ public void defaultBlockSize() {
+
+ assertThat(engine.getBlockSize("java")).isEqualTo(10);
+ }
+
+ @Test
+ public void blockSizeForCobol() {
+ settings.setProperty("sonar.cpd.cobol.minimumLines", "42");
+
+ assertThat(engine.getBlockSize("cobol")).isEqualTo(42);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DuplicationPredicatesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DuplicationPredicatesTest.java
new file mode 100644
index 00000000000..6b6fa4fa0e8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/DuplicationPredicatesTest.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+import com.google.common.base.Predicate;
+import org.junit.Test;
+import org.sonar.duplications.index.CloneGroup;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DuplicationPredicatesTest {
+
+ @Test
+ public void testNumberOfUnitsNotLessThan() {
+ Predicate<CloneGroup> predicate = DuplicationPredicates.numberOfUnitsNotLessThan(5);
+ assertThat(predicate.apply(CloneGroup.builder().setLengthInUnits(6).build())).isTrue();
+ assertThat(predicate.apply(CloneGroup.builder().setLengthInUnits(5).build())).isTrue();
+ assertThat(predicate.apply(CloneGroup.builder().setLengthInUnits(4).build())).isFalse();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/JavaCpdBlockIndexerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/JavaCpdBlockIndexerTest.java
new file mode 100644
index 00000000000..851fb523815
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/cpd/JavaCpdBlockIndexerTest.java
@@ -0,0 +1,106 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.cpd;
+
+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.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.duplications.block.Block;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class JavaCpdBlockIndexerTest {
+ private static final String JAVA = "java";
+
+ @Mock
+ private SonarCpdBlockIndex index;
+
+ @Captor
+ private ArgumentCaptor<List<Block>> blockCaptor;
+
+ private Settings settings;
+ private JavaCpdBlockIndexer engine;
+ private DefaultInputFile file;
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws IOException {
+ MockitoAnnotations.initMocks(this);
+
+ File baseDir = temp.newFolder();
+ DefaultFileSystem fs = new DefaultFileSystem(baseDir);
+ file = new DefaultInputFile("foo", "src/ManyStatements.java").setLanguage(JAVA);
+ fs.add(file);
+ BatchComponentCache batchComponentCache = new BatchComponentCache();
+ batchComponentCache.add(org.sonar.api.resources.File.create("src/Foo.java").setEffectiveKey("foo:src/ManyStatements.java"), null).setInputComponent(file);
+ File ioFile = file.file();
+ FileUtils.copyURLToFile(this.getClass().getResource("ManyStatements.java"), ioFile);
+
+ settings = new Settings();
+ engine = new JavaCpdBlockIndexer(fs, settings, index);
+ }
+
+ @Test
+ public void languageSupported() {
+ JavaCpdBlockIndexer engine = new JavaCpdBlockIndexer(mock(FileSystem.class), new Settings(), index);
+ assertThat(engine.isLanguageSupported(JAVA)).isTrue();
+ assertThat(engine.isLanguageSupported("php")).isFalse();
+ }
+
+ @Test
+ public void testExclusions() {
+ settings.setProperty(CoreProperties.CPD_EXCLUSIONS, "**");
+ engine.index(JAVA);
+ verifyZeroInteractions(index);
+ }
+
+ @Test
+ public void testJavaIndexing() throws Exception {
+ engine.index(JAVA);
+
+ verify(index).insert(eq(file), blockCaptor.capture());
+ List<Block> blockList = blockCaptor.getValue();
+
+ assertThat(blockList).hasSize(26);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilderTest.java
new file mode 100644
index 00000000000..b07671362ce
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/deprecated/perspectives/PerspectiveBuilderTest.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.deprecated.perspectives;
+
+import org.junit.Test;
+import org.sonar.api.component.Perspective;
+import org.sonar.batch.index.BatchComponent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PerspectiveBuilderTest {
+ @Test
+ public void testGetPerspectiveClass() throws Exception {
+ PerspectiveBuilder<FakePerspective> builder = new PerspectiveBuilder<FakePerspective>(FakePerspective.class) {
+ @Override
+ public FakePerspective loadPerspective(Class<FakePerspective> perspectiveClass, BatchComponent component) {
+ return null;
+ }
+ };
+
+ assertThat(builder.getPerspectiveClass()).isEqualTo(FakePerspective.class);
+ }
+
+ static interface FakePerspective extends Perspective {
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/events/BatchStepEventTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/events/BatchStepEventTest.java
new file mode 100644
index 00000000000..3f3738b3900
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/events/BatchStepEventTest.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.events;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class BatchStepEventTest {
+
+ private BatchStepEvent batchStepEvent = new BatchStepEvent("foo", true);
+
+ @Test
+ public void testGetType() {
+ assertThat(batchStepEvent.getType()).isEqualTo(BatchStepHandler.class);
+ }
+
+ @Test
+ public void testDispatch() {
+ BatchStepHandler handler = mock(BatchStepHandler.class);
+ batchStepEvent.dispatch(handler);
+
+ verify(handler).onBatchStep(batchStepEvent);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/events/EventBusTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/events/EventBusTest.java
new file mode 100644
index 00000000000..78fcd4926c2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/events/EventBusTest.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.events;
+
+import org.junit.Test;
+import org.sonar.api.batch.events.EventHandler;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class EventBusTest {
+
+ @Test
+ public void shouldNotifyAboutEvent() {
+ FirstHandler firstHandler = mock(FirstHandler.class);
+ SecondHandler secondHandler = mock(SecondHandler.class);
+ EventBus eventBus = new EventBus(new EventHandler[] { firstHandler, secondHandler });
+
+ FirstEvent firstEvent = new FirstEvent();
+ eventBus.fireEvent(firstEvent);
+ SecondEvent secondEvent = new SecondEvent();
+ eventBus.fireEvent(secondEvent);
+
+ verify(firstHandler).onEvent(firstEvent);
+ verify(secondHandler).onEvent(secondEvent);
+ }
+
+ interface FirstHandler extends EventHandler {
+ void onEvent(FirstEvent event);
+ }
+
+ static class FirstEvent extends BatchEvent<FirstHandler> {
+ @Override
+ protected void dispatch(FirstHandler handler) {
+ handler.onEvent(this);
+ }
+
+ @Override
+ public Class getType() {
+ return FirstHandler.class;
+ }
+ }
+
+ interface SecondHandler extends EventHandler {
+ void onEvent(SecondEvent event);
+ }
+
+ static class SecondEvent extends BatchEvent<SecondHandler> {
+ @Override
+ protected void dispatch(SecondHandler handler) {
+ handler.onEvent(this);
+ }
+
+ @Override
+ public Class getType() {
+ return SecondHandler.class;
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/index/AbstractCachesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/AbstractCachesTest.java
new file mode 100644
index 00000000000..b3e6592d9d2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/AbstractCachesTest.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import org.junit.After;
+
+import org.junit.Before;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import com.google.common.collect.ImmutableMap;
+import org.sonar.api.CoreProperties;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.bootstrap.GlobalTempFolderProvider;
+
+import java.util.Map;
+
+import org.junit.ClassRule;
+import org.junit.rules.TemporaryFolder;
+
+public abstract class AbstractCachesTest {
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ protected static CachesManager cachesManager;
+ protected Caches caches;
+
+ private static CachesManager createCacheOnTemp() {
+ Map<String, String> props = ImmutableMap.of(CoreProperties.WORKING_DIRECTORY, temp.getRoot().getAbsolutePath(),
+ CoreProperties.GLOBAL_WORKING_DIRECTORY, temp.getRoot().getAbsolutePath());
+
+ return new CachesManager(new GlobalTempFolderProvider().provide(new GlobalProperties(props)));
+ }
+
+ @BeforeClass
+ public static void startClass() {
+ cachesManager = createCacheOnTemp();
+ cachesManager.start();
+ }
+
+ @Before
+ public void start() {
+ caches = new Caches(cachesManager);
+ caches.start();
+ }
+
+ @After
+ public void stop() {
+ if (caches != null) {
+ caches.stop();
+ caches = null;
+ }
+ }
+
+ @AfterClass
+ public static void stopClass() {
+ if (cachesManager != null) {
+ cachesManager.stop();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/index/BatchComponentCacheTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/BatchComponentCacheTest.java
new file mode 100644
index 00000000000..8d014b67321
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/BatchComponentCacheTest.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import org.junit.Test;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Resource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class BatchComponentCacheTest {
+ @Test
+ public void should_cache_resource() {
+ BatchComponentCache cache = new BatchComponentCache();
+ String componentKey = "struts:src/org/struts/Action.java";
+ Resource resource = File.create("org/struts/Action.java").setEffectiveKey(componentKey);
+ cache.add(resource, null);
+
+ assertThat(cache.get(componentKey).resource()).isSameAs(resource);
+ assertThat(cache.get("other")).isNull();
+ }
+
+ @Test
+ public void should_fail_if_missing_component_key() {
+ BatchComponentCache cache = new BatchComponentCache();
+ Resource resource = File.create("org/struts/Action.java").setEffectiveKey(null);
+ try {
+ cache.add(resource, null);
+ fail();
+ } catch (IllegalStateException e) {
+ // success
+ assertThat(e).hasMessage("Missing resource effective key");
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/index/BucketTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/BucketTest.java
new file mode 100644
index 00000000000..a6a5f900088
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/BucketTest.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import org.junit.Test;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.File;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class BucketTest {
+
+ Directory directory = Directory.create("org/foo");
+ File javaFile = File.create("org/foo/Bar.java");
+ Metric ncloc = new Metric("ncloc");
+
+ @Test
+ public void shouldManageRelationships() {
+ Bucket packageBucket = new Bucket(directory);
+ Bucket fileBucket = new Bucket(javaFile);
+ fileBucket.setParent(packageBucket);
+
+ assertThat(fileBucket.getParent()).isEqualTo(packageBucket);
+ assertThat(packageBucket.getChildren()).containsExactly(fileBucket);
+ }
+
+ @Test
+ public void shouldBeEquals() {
+ assertEquals(new Bucket(directory), new Bucket(directory));
+ assertEquals(new Bucket(directory).hashCode(), new Bucket(directory).hashCode());
+ }
+
+ @Test
+ public void shouldNotBeEquals() {
+ assertFalse(new Bucket(directory).equals(new Bucket(javaFile)));
+ assertThat(new Bucket(directory).hashCode()).isNotEqualTo(new Bucket(javaFile).hashCode());
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CacheTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CacheTest.java
new file mode 100644
index 00000000000..95ddcb8b766
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CacheTest.java
@@ -0,0 +1,250 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import com.google.common.collect.Iterables;
+import org.junit.Test;
+import org.sonar.batch.index.Cache.Entry;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CacheTest extends AbstractCachesTest {
+
+ @Test
+ public void one_part_key() {
+ Cache<String> cache = caches.createCache("capitals");
+
+ assertThat(cache.get("france")).isNull();
+
+ cache.put("france", "paris");
+ cache.put("italy", "rome");
+ assertThat(cache.get("france")).isEqualTo("paris");
+ assertThat(cache.keySet()).containsOnly("france", "italy");
+ assertThat(cache.keySet("france")).isEmpty();
+ Iterable<String> values = cache.values();
+ assertThat(values).containsOnly("paris", "rome");
+ assertThat(values).containsOnly("paris", "rome");
+ assertThat(cache.containsKey("france")).isTrue();
+
+ Iterable<Entry<String>> iterable = cache.entries();
+ Cache.Entry[] entries = Iterables.toArray(iterable, Cache.Entry.class);
+ assertThat(entries).hasSize(2);
+ assertThat(iterable).hasSize(2);
+ assertThat(entries[0].key()[0]).isEqualTo("france");
+ assertThat(entries[0].value()).isEqualTo("paris");
+ assertThat(entries[1].key()[0]).isEqualTo("italy");
+ assertThat(entries[1].value()).isEqualTo("rome");
+
+ cache.remove("france");
+ assertThat(cache.get("france")).isNull();
+ assertThat(cache.get("italy")).isEqualTo("rome");
+ assertThat(cache.keySet()).containsOnly("italy");
+ assertThat(cache.keySet("france")).isEmpty();
+ assertThat(cache.containsKey("france")).isFalse();
+ assertThat(cache.containsKey("italy")).isTrue();
+ assertThat(values).containsOnly("rome");
+
+ cache.clear();
+ assertThat(values).isEmpty();
+ }
+
+ @Test
+ public void test_key_being_prefix_of_another_key() throws Exception {
+ Cache<String> cache = caches.createCache("components");
+
+ cache.put("struts-el:org.apache.strutsel.taglib.html.ELButtonTag", "the Tag");
+ cache.put("struts-el:org.apache.strutsel.taglib.html.ELButtonTagBeanInfo", "the BeanInfo");
+
+ assertThat(cache.get("struts-el:org.apache.strutsel.taglib.html.ELButtonTag")).isEqualTo("the Tag");
+ assertThat(cache.get("struts-el:org.apache.strutsel.taglib.html.ELButtonTagBeanInfo")).isEqualTo("the BeanInfo");
+ }
+
+ @Test
+ public void two_parts_key() {
+ Cache<String> cache = caches.createCache("capitals");
+
+ assertThat(cache.get("europe", "france")).isNull();
+
+ cache.put("europe", "france", "paris");
+ cache.put("europe", "italy", "rome");
+ cache.put("asia", "china", "pekin");
+ assertThat(cache.get("europe")).isNull();
+ assertThat(cache.get("europe", "france")).isEqualTo("paris");
+ assertThat(cache.get("europe", "italy")).isEqualTo("rome");
+ assertThat(cache.get("europe")).isNull();
+ assertThat(cache.keySet("europe")).containsOnly("france", "italy");
+ assertThat(cache.keySet()).containsOnly("europe", "asia");
+ assertThat(cache.containsKey("europe")).isFalse();
+ assertThat(cache.containsKey("europe", "france")).isTrue();
+ assertThat(cache.containsKey("europe", "spain")).isFalse();
+ assertThat(cache.values()).containsOnly("paris", "rome", "pekin");
+ assertThat(cache.values("america")).isEmpty();
+ assertThat(cache.values("europe")).containsOnly("paris", "rome");
+ assertThat(cache.values("oceania")).isEmpty();
+
+ Iterable<Entry<String>> iterable = cache.entries();
+ Cache.Entry[] allEntries = Iterables.toArray(iterable, Cache.Entry.class);
+ assertThat(allEntries).hasSize(3);
+ assertThat(iterable).hasSize(3);
+ assertThat(allEntries[0].key()).isEqualTo(new String[] {"asia", "china"});
+ assertThat(allEntries[0].value()).isEqualTo("pekin");
+ assertThat(allEntries[1].key()).isEqualTo(new String[] {"europe", "france"});
+ assertThat(allEntries[1].value()).isEqualTo("paris");
+ assertThat(allEntries[2].key()).isEqualTo(new String[] {"europe", "italy"});
+ assertThat(allEntries[2].value()).isEqualTo("rome");
+
+ Iterable<Entry<String>> iterable2 = cache.entries("europe");
+ Cache.Entry[] subEntries = Iterables.toArray(iterable2, Cache.Entry.class);
+ assertThat(subEntries).hasSize(2);
+ assertThat(iterable2).hasSize(2);
+ assertThat(subEntries[0].key()).isEqualTo(new String[] {"europe", "france"});
+ assertThat(subEntries[0].value()).isEqualTo("paris");
+ assertThat(subEntries[1].key()).isEqualTo(new String[] {"europe", "italy"});
+ assertThat(subEntries[1].value()).isEqualTo("rome");
+
+ cache.remove("europe", "france");
+ assertThat(cache.values()).containsOnly("rome", "pekin");
+ assertThat(cache.get("europe", "france")).isNull();
+ assertThat(cache.get("europe", "italy")).isEqualTo("rome");
+ assertThat(cache.containsKey("europe", "france")).isFalse();
+ assertThat(cache.keySet("europe")).containsOnly("italy");
+
+ cache.clear("america");
+ assertThat(cache.keySet()).containsOnly("europe", "asia");
+ cache.clear();
+ assertThat(cache.keySet()).isEmpty();
+ }
+
+ @Test
+ public void three_parts_key() {
+ Cache<String> cache = caches.createCache("places");
+ assertThat(cache.get("europe", "france", "paris")).isNull();
+
+ cache.put("europe", "france", "paris", "eiffel tower");
+ cache.put("europe", "france", "annecy", "lake");
+ cache.put("europe", "france", "poitiers", "notre dame");
+ cache.put("europe", "italy", "rome", "colosseum");
+ cache.put("europe2", "ukrania", "kiev", "dunno");
+ cache.put("asia", "china", "pekin", "great wall");
+ cache.put("america", "us", "new york", "empire state building");
+ assertThat(cache.get("europe")).isNull();
+ assertThat(cache.get("europe", "france")).isNull();
+ assertThat(cache.get("europe", "france", "paris")).isEqualTo("eiffel tower");
+ assertThat(cache.get("europe", "france", "annecy")).isEqualTo("lake");
+ assertThat(cache.get("europe", "italy", "rome")).isEqualTo("colosseum");
+ assertThat(cache.keySet()).containsOnly("europe", "asia", "america", "europe2");
+ assertThat(cache.keySet("europe")).containsOnly("france", "italy");
+ assertThat(cache.keySet("europe", "france")).containsOnly("annecy", "paris", "poitiers");
+ assertThat(cache.containsKey("europe")).isFalse();
+ assertThat(cache.containsKey("europe", "france")).isFalse();
+ assertThat(cache.containsKey("europe", "france", "annecy")).isTrue();
+ assertThat(cache.containsKey("europe", "france", "biarritz")).isFalse();
+ assertThat(cache.values()).containsOnly("eiffel tower", "lake", "colosseum", "notre dame", "great wall", "empire state building", "dunno");
+ assertThat(cache.values("europe")).containsOnly("eiffel tower", "lake", "colosseum", "notre dame");
+ assertThat(cache.values("europe", "france")).containsOnly("eiffel tower", "lake", "notre dame");
+
+ Iterable<Entry<String>> iterable = cache.entries();
+ Cache.Entry[] allEntries = Iterables.toArray(iterable, Cache.Entry.class);
+ assertThat(allEntries).hasSize(7);
+ assertThat(iterable).hasSize(7);
+ assertThat(allEntries[0].key()).isEqualTo(new String[] {"america", "us", "new york"});
+ assertThat(allEntries[0].value()).isEqualTo("empire state building");
+ assertThat(allEntries[1].key()).isEqualTo(new String[] {"asia", "china", "pekin"});
+ assertThat(allEntries[1].value()).isEqualTo("great wall");
+ assertThat(allEntries[2].key()).isEqualTo(new String[] {"europe", "france", "annecy"});
+ assertThat(allEntries[2].value()).isEqualTo("lake");
+ assertThat(allEntries[3].key()).isEqualTo(new String[] {"europe", "france", "paris"});
+ assertThat(allEntries[3].value()).isEqualTo("eiffel tower");
+ assertThat(allEntries[4].key()).isEqualTo(new String[] {"europe", "france", "poitiers"});
+ assertThat(allEntries[4].value()).isEqualTo("notre dame");
+ assertThat(allEntries[5].key()).isEqualTo(new String[] {"europe", "italy", "rome"});
+ assertThat(allEntries[5].value()).isEqualTo("colosseum");
+
+ Iterable<Entry<String>> iterable2 = cache.entries("europe");
+ Cache.Entry[] subEntries = Iterables.toArray(iterable2, Cache.Entry.class);
+ assertThat(subEntries).hasSize(4);
+ assertThat(iterable2).hasSize(4);
+ assertThat(subEntries[0].key()).isEqualTo(new String[] {"europe", "france", "annecy"});
+ assertThat(subEntries[0].value()).isEqualTo("lake");
+ assertThat(subEntries[1].key()).isEqualTo(new String[] {"europe", "france", "paris"});
+ assertThat(subEntries[1].value()).isEqualTo("eiffel tower");
+ assertThat(subEntries[2].key()).isEqualTo(new String[] {"europe", "france", "poitiers"});
+ assertThat(subEntries[2].value()).isEqualTo("notre dame");
+ assertThat(subEntries[3].key()).isEqualTo(new String[] {"europe", "italy", "rome"});
+ assertThat(subEntries[3].value()).isEqualTo("colosseum");
+
+ cache.remove("europe", "france", "annecy");
+ assertThat(cache.values()).containsOnly("eiffel tower", "colosseum", "notre dame", "great wall", "empire state building", "dunno");
+ assertThat(cache.values("europe")).containsOnly("eiffel tower", "colosseum", "notre dame");
+ assertThat(cache.values("europe", "france")).containsOnly("eiffel tower", "notre dame");
+ assertThat(cache.get("europe", "france", "annecy")).isNull();
+ assertThat(cache.get("europe", "italy", "rome")).isEqualTo("colosseum");
+ assertThat(cache.containsKey("europe", "france")).isFalse();
+
+ cache.clear("europe", "italy");
+ assertThat(cache.values()).containsOnly("eiffel tower", "notre dame", "great wall", "empire state building", "dunno");
+
+ cache.clear("europe");
+ assertThat(cache.values()).containsOnly("great wall", "empire state building", "dunno");
+
+ cache.clear();
+ assertThat(cache.values()).isEmpty();
+ }
+
+ @Test
+ public void remove_versus_clear() {
+ Cache<String> cache = caches.createCache("capitals");
+ cache.put("europe", "france", "paris");
+ cache.put("europe", "italy", "rome");
+
+ // remove("europe") does not remove sub-keys
+ cache.remove("europe");
+ assertThat(cache.values()).containsOnly("paris", "rome");
+
+ // clear("europe") removes sub-keys
+ cache.clear("europe");
+ assertThat(cache.values()).isEmpty();
+ }
+
+ @Test
+ public void empty_cache() {
+ Cache<String> cache = caches.createCache("empty");
+
+ assertThat(cache.get("foo")).isNull();
+ assertThat(cache.get("foo", "bar")).isNull();
+ assertThat(cache.get("foo", "bar", "baz")).isNull();
+ assertThat(cache.keySet()).isEmpty();
+ assertThat(cache.keySet("foo")).isEmpty();
+ assertThat(cache.containsKey("foo")).isFalse();
+ assertThat(cache.containsKey("foo", "bar")).isFalse();
+ assertThat(cache.containsKey("foo", "bar", "baz")).isFalse();
+ assertThat(cache.values()).isEmpty();
+ assertThat(cache.values("foo")).isEmpty();
+
+ // do not fail
+ cache.remove("foo");
+ cache.remove("foo", "bar");
+ cache.remove("foo", "bar", "baz");
+ cache.clear("foo");
+ cache.clear("foo", "bar");
+ cache.clear("foo", "bar", "baz");
+ cache.clear();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesManagerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesManagerTest.java
new file mode 100644
index 00000000000..177c6f1e357
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesManagerTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CachesManagerTest extends AbstractCachesTest {
+ @Test
+ public void should_stop_and_clean_temp_dir() {
+ File tempDir = cachesManager.tempDir();
+ assertThat(tempDir).isDirectory().exists();
+ assertThat(cachesManager.persistit()).isNotNull();
+ assertThat(cachesManager.persistit().isInitialized()).isTrue();
+
+ cachesManager.stop();
+
+ assertThat(tempDir).doesNotExist();
+ assertThat(cachesManager.tempDir()).isNull();
+ assertThat(cachesManager.persistit()).isNull();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesTest.java
new file mode 100644
index 00000000000..2a01ac38025
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/CachesTest.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import java.io.Serializable;
+
+import com.persistit.exception.PersistitException;
+import org.junit.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class CachesTest extends AbstractCachesTest {
+ @Test
+ public void should_create_cache() {
+ Cache<Element> cache = caches.createCache("foo");
+ assertThat(cache).isNotNull();
+ }
+
+ @Test
+ public void should_not_create_cache_twice() {
+ caches.<Element>createCache("foo");
+ try {
+ caches.<Element>createCache("foo");
+ fail();
+ } catch (IllegalStateException e) {
+ // ok
+ }
+ }
+
+ @Test
+ public void should_clean_resources() {
+ Cache<String> c = caches.<String>createCache("test1");
+ for (int i = 0; i < 1_000_000; i++) {
+ c.put("a" + i, "a" + i);
+ }
+
+ caches.stop();
+
+ // manager continues up
+ assertThat(cachesManager.persistit().isInitialized()).isTrue();
+
+ caches = new Caches(cachesManager);
+ caches.start();
+ caches.createCache("test1");
+ }
+
+ @Test
+ public void leak_test() throws PersistitException {
+ caches.stop();
+
+ int len = 1 * 1024 * 1024;
+ StringBuilder sb = new StringBuilder(len);
+ for (int i = 0; i < len; i++) {
+ sb.append("a");
+ }
+
+ for (int i = 0; i < 3; i++) {
+ caches = new Caches(cachesManager);
+ caches.start();
+ Cache<String> c = caches.<String>createCache("test" + i);
+ c.put("key" + i, sb.toString());
+ cachesManager.persistit().flush();
+
+ caches.stop();
+ }
+ }
+
+ private static class Element implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/index/DefaultIndexTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/DefaultIndexTest.java
new file mode 100644
index 00000000000..8dae9e19ad0
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/index/DefaultIndexTest.java
@@ -0,0 +1,163 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.index;
+
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.MeasuresFilters;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Java;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.batch.DefaultProjectTree;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.batch.sensor.DefaultSensorStorage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultIndexTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ DefaultIndex index = null;
+ Rule rule;
+ RuleFinder ruleFinder;
+ Project project;
+ Project moduleA;
+ Project moduleB;
+ Project moduleB1;
+
+ private java.io.File baseDir;
+
+ @Before
+ public void createIndex() throws IOException {
+ ruleFinder = mock(RuleFinder.class);
+
+ DefaultProjectTree projectTree = mock(DefaultProjectTree.class);
+ BatchComponentCache resourceCache = new BatchComponentCache();
+ index = new DefaultIndex(resourceCache, projectTree, mock(MeasureCache.class), new PathResolver());
+
+ baseDir = temp.newFolder();
+ project = new Project("project");
+ when(projectTree.getProjectDefinition(project)).thenReturn(ProjectDefinition.create().setBaseDir(baseDir));
+ moduleA = new Project("moduleA").setParent(project);
+ when(projectTree.getProjectDefinition(moduleA)).thenReturn(ProjectDefinition.create().setBaseDir(new java.io.File(baseDir, "moduleA")));
+ moduleB = new Project("moduleB").setParent(project);
+ when(projectTree.getProjectDefinition(moduleB)).thenReturn(ProjectDefinition.create().setBaseDir(new java.io.File(baseDir, "moduleB")));
+ moduleB1 = new Project("moduleB1").setParent(moduleB);
+ when(projectTree.getProjectDefinition(moduleB1)).thenReturn(ProjectDefinition.create().setBaseDir(new java.io.File(baseDir, "moduleB/moduleB1")));
+
+ RulesProfile rulesProfile = RulesProfile.create();
+ rule = Rule.create("repoKey", "ruleKey", "Rule");
+ rule.setId(1);
+ rulesProfile.activateRule(rule, null);
+ index.setCurrentProject(project, mock(DefaultSensorStorage.class));
+ index.doStart(project);
+ }
+
+ @Test
+ public void shouldIndexParentOfDeprecatedFiles() {
+ File file = File.create("src/org/foo/Bar.java", null, false);
+ assertThat(index.index(file)).isTrue();
+
+ Directory reference = Directory.create("src/org/foo");
+ assertThat(index.getResource(reference).getName()).isEqualTo("src/org/foo");
+ assertThat(index.isIndexed(reference, true)).isTrue();
+ assertThat(index.isExcluded(reference)).isFalse();
+ assertThat(index.getChildren(reference)).hasSize(1);
+ assertThat(index.getParent(reference)).isInstanceOf(Project.class);
+ }
+
+ @Test
+ public void shouldIndexTreeOfResources() {
+ Directory directory = Directory.create("src/org/foo");
+ File file = File.create("src/org/foo/Bar.java", Java.INSTANCE, false);
+
+ assertThat(index.index(directory)).isTrue();
+ assertThat(index.index(file, directory)).isTrue();
+
+ File fileRef = File.create("src/org/foo/Bar.java", null, false);
+ assertThat(index.getResource(fileRef).getKey()).isEqualTo("src/org/foo/Bar.java");
+ assertThat(index.getResource(fileRef).getLanguage().getKey()).isEqualTo("java");
+ assertThat(index.isIndexed(fileRef, true)).isTrue();
+ assertThat(index.isExcluded(fileRef)).isFalse();
+ assertThat(index.getChildren(fileRef)).isEmpty();
+ assertThat(index.getParent(fileRef)).isInstanceOf(Directory.class);
+ }
+
+ @Test
+ public void shouldGetSource() throws Exception {
+ Directory directory = Directory.create("src/org/foo");
+ File file = File.create("src/org/foo/Bar.java", Java.INSTANCE, false);
+ FileUtils.write(new java.io.File(baseDir, "src/org/foo/Bar.java"), "Foo bar");
+
+ assertThat(index.index(directory)).isTrue();
+ assertThat(index.index(file, directory)).isTrue();
+
+ File fileRef = File.create("src/org/foo/Bar.java", null, false);
+ assertThat(index.getSource(fileRef)).isEqualTo("Foo bar");
+ }
+
+ @Test
+ public void shouldNotIndexResourceIfParentNotIndexed() {
+ Directory directory = Directory.create("src/org/other");
+ File file = File.create("src/org/foo/Bar.java", null, false);
+
+ assertThat(index.index(file, directory)).isFalse();
+
+ File fileRef = File.create("src/org/foo/Bar.java", null, false);
+ assertThat(index.isIndexed(directory, true)).isFalse();
+ assertThat(index.isIndexed(fileRef, true)).isFalse();
+ assertThat(index.isExcluded(fileRef)).isFalse();
+ assertThat(index.getChildren(fileRef)).isEmpty();
+ assertThat(index.getParent(fileRef)).isNull();
+ }
+
+ @Test
+ public void shouldNotIndexResourceWhenAddingMeasure() {
+ Resource dir = Directory.create("src/org/foo");
+ index.addMeasure(dir, new Measure("ncloc").setValue(50.0));
+
+ assertThat(index.isIndexed(dir, true)).isFalse();
+ assertThat(index.getMeasures(dir, MeasuresFilters.metric("ncloc"))).isNull();
+ }
+
+ @Test
+ public void shouldComputePathOfIndexedModules() {
+ assertThat(index.getResource(project).getPath()).isNull();
+ assertThat(index.getResource(moduleA).getPath()).isEqualTo("moduleA");
+ assertThat(index.getResource(moduleB).getPath()).isEqualTo("moduleB");
+ assertThat(index.getResource(moduleB1).getPath()).isEqualTo("moduleB1");
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultFilterableIssueTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultFilterableIssueTest.java
new file mode 100644
index 00000000000..daded262498
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultFilterableIssueTest.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+import java.util.Date;
+
+import static org.mockito.Mockito.mock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.resources.Project;
+import org.sonar.scanner.protocol.Constants.Severity;
+import org.sonar.scanner.protocol.output.ScannerReport.Issue;
+
+public class DefaultFilterableIssueTest {
+ private DefaultFilterableIssue issue;
+ private Project mockedProject;
+ private String componentKey;
+ private Issue rawIssue;
+
+ @Before
+ public void setUp() {
+ mockedProject = mock(Project.class);
+ componentKey = "component";
+ }
+
+ private Issue createIssue() {
+ Issue.Builder builder = Issue.newBuilder();
+
+ builder.setGap(3.0);
+ builder.setLine(30);
+ builder.setSeverity(Severity.MAJOR);
+ return builder.build();
+ }
+
+ private Issue createIssueWithoutFields() {
+ Issue.Builder builder = Issue.newBuilder();
+ builder.setSeverity(Severity.MAJOR);
+ return builder.build();
+ }
+
+ @Test
+ public void testRoundTrip() {
+ rawIssue = createIssue();
+ issue = new DefaultFilterableIssue(mockedProject, rawIssue, componentKey);
+
+ when(mockedProject.getAnalysisDate()).thenReturn(new Date(10_000));
+ when(mockedProject.getEffectiveKey()).thenReturn("projectKey");
+
+ assertThat(issue.componentKey()).isEqualTo(componentKey);
+ assertThat(issue.creationDate()).isEqualTo(new Date(10_000));
+ assertThat(issue.line()).isEqualTo(30);
+ assertThat(issue.projectKey()).isEqualTo("projectKey");
+ assertThat(issue.effortToFix()).isEqualTo(3.0);
+ assertThat(issue.severity()).isEqualTo("MAJOR");
+ }
+
+ @Test
+ public void nullValues() {
+ rawIssue = createIssueWithoutFields();
+ issue = new DefaultFilterableIssue(mockedProject, rawIssue, componentKey);
+
+ assertThat(issue.line()).isNull();
+ assertThat(issue.effortToFix()).isNull();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueCallbackTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueCallbackTest.java
new file mode 100644
index 00000000000..7be36bd3bc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueCallbackTest.java
@@ -0,0 +1,137 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.bootstrapper.IssueListener.Issue;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Mock;
+import org.sonar.api.batch.rule.Rules;
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.scanner.protocol.input.ScannerInput;
+import org.sonar.batch.bootstrapper.IssueListener;
+import org.junit.Before;
+import com.google.common.collect.ImmutableList;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Test;
+
+public class DefaultIssueCallbackTest {
+ @Mock
+ private IssueCache issueCache;
+ @Mock
+ private UserRepositoryLoader userRepository;
+ @Mock
+ private Rules rules;
+
+ private TrackedIssue issue;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ RuleKey ruleKey = RuleKey.of("repo", "key");
+ issue = new TrackedIssue();
+ issue.setKey("key");
+ issue.setAssignee("user");
+ issue.setRuleKey(ruleKey);
+
+ when(issueCache.all()).thenReturn(ImmutableList.of(issue));
+
+ ScannerInput.User.Builder userBuilder = ScannerInput.User.newBuilder();
+ userBuilder.setLogin("user");
+ userBuilder.setName("name");
+ when(userRepository.load("user")).thenReturn(userBuilder.build());
+
+ Rule r = mock(Rule.class);
+ when(r.name()).thenReturn("rule name");
+ when(rules.find(ruleKey)).thenReturn(r);
+ }
+
+ @Test
+ public void testWithoutListener() {
+ DefaultIssueCallback issueCallback = new DefaultIssueCallback(issueCache, userRepository, rules);
+ issueCallback.execute();
+ }
+
+ @Test
+ public void testWithListener() {
+ final List<IssueListener.Issue> issueList = new LinkedList<>();
+ IssueListener listener = new IssueListener() {
+ @Override
+ public void handle(Issue issue) {
+ issueList.add(issue);
+ }
+ };
+
+ DefaultIssueCallback issueCallback = new DefaultIssueCallback(issueCache, listener, userRepository, rules);
+ issueCallback.execute();
+
+ assertThat(issueList).hasSize(1);
+ Issue callbackIssue = issueList.get(0);
+
+ assertThat(callbackIssue.getAssigneeName()).isEqualTo("name");
+ assertThat(callbackIssue.getRuleName()).isEqualTo("rule name");
+ }
+
+ @Test
+ public void testWithNulls() {
+ final List<IssueListener.Issue> issueList = new LinkedList<>();
+ IssueListener listener = new IssueListener() {
+ @Override
+ public void handle(Issue issue) {
+ issueList.add(issue);
+ }
+ };
+
+ issue.setKey(null);
+ issue.setAssignee(null);
+
+ DefaultIssueCallback issueCallback = new DefaultIssueCallback(issueCache, listener, userRepository, rules);
+ issueCallback.execute();
+ }
+
+ @Test
+ public void testDecorationNotFound() {
+ final List<IssueListener.Issue> issueList = new LinkedList<>();
+ IssueListener listener = new IssueListener() {
+ @Override
+ public void handle(Issue issue) {
+ issueList.add(issue);
+ }
+ };
+
+ when(userRepository.load(any(String.class))).thenReturn(null);
+ when(rules.find(any(RuleKey.class))).thenReturn(null);
+
+ DefaultIssueCallback issueCallback = new DefaultIssueCallback(issueCache, listener, userRepository, rules);
+ issueCallback.execute();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueFilterChainTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueFilterChainTest.java
new file mode 100644
index 00000000000..041bc8493a2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultIssueFilterChainTest.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import org.junit.Test;
+import org.sonar.api.scan.issue.filter.IssueFilter;
+import org.sonar.api.scan.issue.filter.IssueFilterChain;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+public class DefaultIssueFilterChainTest {
+ private final FilterableIssue issue = mock(FilterableIssue.class);
+
+ @Test
+ public void should_accept_when_no_filter() {
+ assertThat(new DefaultIssueFilterChain().accept(issue)).isTrue();
+ }
+
+ class PassingFilter implements IssueFilter {
+ @Override
+ public boolean accept(FilterableIssue issue, IssueFilterChain chain) {
+ return chain.accept(issue);
+ }
+ }
+
+ class AcceptingFilter implements IssueFilter {
+ @Override
+ public boolean accept(FilterableIssue issue, IssueFilterChain chain) {
+ return true;
+ }
+ }
+
+ class RefusingFilter implements IssueFilter {
+ @Override
+ public boolean accept(FilterableIssue issue, IssueFilterChain chain) {
+ return false;
+ }
+ }
+
+ class FailingFilter implements IssueFilter {
+ @Override
+ public boolean accept(FilterableIssue issue, IssueFilterChain chain) {
+ fail();
+ return false;
+ }
+
+ }
+
+ @Test
+ public void should_accept_if_all_filters_pass() {
+ assertThat(new DefaultIssueFilterChain(
+ new PassingFilter(),
+ new PassingFilter(),
+ new PassingFilter()
+ ).accept(issue)).isTrue();
+ }
+
+ @Test
+ public void should_accept_and_not_go_further_if_filter_accepts() {
+ assertThat(new DefaultIssueFilterChain(
+ new PassingFilter(),
+ new AcceptingFilter(),
+ new FailingFilter()
+ ).accept(issue)).isTrue();
+ }
+
+ @Test
+ public void should_refuse_and_not_go_further_if_filter_refuses() {
+ assertThat(new DefaultIssueFilterChain(
+ new PassingFilter(),
+ new RefusingFilter(),
+ new FailingFilter()
+ ).accept(issue)).isFalse();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultProjectIssuesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultProjectIssuesTest.java
new file mode 100644
index 00000000000..9448206b1d6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DefaultProjectIssuesTest.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import com.google.common.collect.Lists;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultProjectIssuesTest {
+
+ static final RuleKey SQUID_RULE_KEY = RuleKey.of("squid", "AvoidCycle");
+
+ IssueCache cache = mock(IssueCache.class);
+ DefaultProjectIssues projectIssues = new DefaultProjectIssues(cache);
+
+ @Test
+ public void should_get_all_issues() {
+ DefaultIssue issueOnModule = new DefaultIssue().setKey("1").setRuleKey(SQUID_RULE_KEY).setComponentKey("org.apache:struts-core");
+ DefaultIssue issueInModule = new DefaultIssue().setKey("2").setRuleKey(SQUID_RULE_KEY).setComponentKey("org.apache:struts-core:Action");
+ DefaultIssue resolvedIssueInModule = new DefaultIssue().setKey("3").setRuleKey(SQUID_RULE_KEY).setComponentKey("org.apache:struts-core:Action")
+ .setResolution(Issue.RESOLUTION_FIXED);
+
+ DefaultIssue issueOnRoot = new DefaultIssue().setKey("4").setRuleKey(SQUID_RULE_KEY).setSeverity(Severity.CRITICAL).setComponentKey("org.apache:struts");
+ DefaultIssue issueInRoot = new DefaultIssue().setKey("5").setRuleKey(SQUID_RULE_KEY).setSeverity(Severity.CRITICAL).setComponentKey("org.apache:struts:FileInRoot");
+ when(cache.all()).thenReturn(Arrays.<TrackedIssue>asList(
+ toTrackedIssue(issueOnRoot), toTrackedIssue(issueInRoot),
+ toTrackedIssue(issueOnModule), toTrackedIssue(issueInModule), toTrackedIssue(resolvedIssueInModule)
+ ));
+
+ // unresolved issues
+ List<Issue> issues = Lists.newArrayList(projectIssues.issues());
+ assertThat(issues).containsOnly(issueOnRoot, issueInRoot, issueInModule, issueOnModule);
+
+ List<Issue> resolvedIssues = Lists.newArrayList(projectIssues.resolvedIssues());
+ assertThat(resolvedIssues).containsOnly(resolvedIssueInModule);
+ }
+
+ private TrackedIssue toTrackedIssue(DefaultIssue issue) {
+ TrackedIssue trackedIssue = new TrackedIssue();
+
+ trackedIssue.setKey(issue.key());
+ trackedIssue.setRuleKey(issue.ruleKey());
+ trackedIssue.setComponentKey(issue.componentKey());
+ trackedIssue.setSeverity(issue.severity());
+ trackedIssue.setResolution(issue.resolution());
+
+ return trackedIssue;
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilterTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilterTest.java
new file mode 100644
index 00000000000..b4818adae0e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueAdapterForFilterTest.java
@@ -0,0 +1,157 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.util.Date;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.resources.Project;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.scanner.protocol.Constants.Severity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+public class DeprecatedIssueAdapterForFilterTest {
+
+ private static final String PROJECT_KEY = "foo";
+ private static final Date ANALYSIS_DATE = new Date();
+ private static final String COMPONENT_KEY = "foo:src/Foo.java";
+
+ @Test
+ public void improve_coverage() {
+ DeprecatedIssueAdapterForFilter issue = new DeprecatedIssueAdapterForFilter(new Project(PROJECT_KEY).setAnalysisDate(ANALYSIS_DATE),
+ org.sonar.scanner.protocol.output.ScannerReport.Issue.newBuilder()
+ .setRuleRepository("repo")
+ .setRuleKey("key")
+ .setSeverity(Severity.BLOCKER)
+ .setMsg("msg")
+ .build(),
+ COMPONENT_KEY);
+ DeprecatedIssueAdapterForFilter issue2 = new DeprecatedIssueAdapterForFilter(new Project(PROJECT_KEY).setAnalysisDate(ANALYSIS_DATE),
+ org.sonar.scanner.protocol.output.ScannerReport.Issue.newBuilder()
+ .setRuleRepository("repo")
+ .setRuleKey("key")
+ .setSeverity(Severity.BLOCKER)
+ .setMsg("msg")
+ .setLine(1)
+ .setGap(2.0)
+ .build(),
+ COMPONENT_KEY);
+
+ try {
+ issue.key();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ assertThat(issue.componentKey()).isEqualTo(COMPONENT_KEY);
+ assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "key"));
+
+ try {
+ issue.language();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ assertThat(issue.severity()).isEqualTo("BLOCKER");
+ assertThat(issue.message()).isEqualTo("msg");
+ assertThat(issue.line()).isNull();
+ assertThat(issue2.line()).isEqualTo(1);
+ assertThat(issue.effortToFix()).isNull();
+ assertThat(issue2.effortToFix()).isEqualTo(2.0);
+ assertThat(issue.status()).isEqualTo(Issue.STATUS_OPEN);
+ assertThat(issue.resolution()).isNull();
+
+ try {
+ issue.reporter();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ assertThat(issue.assignee()).isNull();
+ assertThat(issue.creationDate()).isEqualTo(ANALYSIS_DATE);
+ assertThat(issue.updateDate()).isNull();
+ assertThat(issue.closeDate()).isNull();
+ assertThat(issue.attribute(PROJECT_KEY)).isNull();
+
+ try {
+ issue.authorLogin();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ try {
+ issue.actionPlanKey();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ try {
+ issue.comments();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ try {
+ issue.isNew();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ try {
+ issue.debt();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ assertThat(issue.projectKey()).isEqualTo(PROJECT_KEY);
+
+ try {
+ issue.projectUuid();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ try {
+ issue.componentUuid();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+
+ try {
+ issue.tags();
+ fail("Should be unsupported");
+ } catch (Exception e) {
+ assertThat(e).isExactlyInstanceOf(UnsupportedOperationException.class).hasMessage("Not available for issues filters");
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueFilterChainTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueFilterChainTest.java
new file mode 100644
index 00000000000..c80b1d64a82
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/DeprecatedIssueFilterChainTest.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.batch.IssueFilter;
+import org.sonar.api.issue.batch.IssueFilterChain;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+public class DeprecatedIssueFilterChainTest {
+
+ private final Issue issue = mock(Issue.class);
+
+ @Test
+ public void should_accept_when_no_filter() {
+ assertThat(new DeprecatedIssueFilterChain().accept(issue)).isTrue();
+ }
+
+ class PassingFilter implements IssueFilter {
+ @Override
+ public boolean accept(Issue issue, IssueFilterChain chain) {
+ return chain.accept(issue);
+ }
+ }
+
+ class AcceptingFilter implements IssueFilter {
+ @Override
+ public boolean accept(Issue issue, IssueFilterChain chain) {
+ return true;
+ }
+ }
+
+ class RefusingFilter implements IssueFilter {
+ @Override
+ public boolean accept(Issue issue, IssueFilterChain chain) {
+ return false;
+ }
+ }
+
+ class FailingFilter implements IssueFilter {
+ @Override
+ public boolean accept(Issue issue, IssueFilterChain chain) {
+ fail();
+ return false;
+ }
+
+ }
+
+ @Test
+ public void should_accept_if_all_filters_pass() {
+ assertThat(new DeprecatedIssueFilterChain(
+ new PassingFilter(),
+ new PassingFilter(),
+ new PassingFilter()
+ ).accept(issue)).isTrue();
+ }
+
+ @Test
+ public void should_accept_and_not_go_further_if_filter_accepts() {
+ assertThat(new DeprecatedIssueFilterChain(
+ new PassingFilter(),
+ new AcceptingFilter(),
+ new FailingFilter()
+ ).accept(issue)).isTrue();
+ }
+
+ @Test
+ public void should_refuse_and_not_go_further_if_filter_refuses() {
+ assertThat(new DeprecatedIssueFilterChain(
+ new PassingFilter(),
+ new RefusingFilter(),
+ new FailingFilter()
+ ).accept(issue)).isFalse();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssuableFactoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssuableFactoryTest.java
new file mode 100644
index 00000000000..65856e1e808
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssuableFactoryTest.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.junit.Test;
+import org.sonar.api.issue.Issuable;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.DefaultProjectTree;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.sensor.DefaultSensorContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class IssuableFactoryTest {
+
+ ModuleIssues moduleIssues = mock(ModuleIssues.class);
+ DefaultProjectTree projectTree = mock(DefaultProjectTree.class);
+
+ @Test
+ public void file_should_be_issuable() {
+ IssuableFactory factory = new IssuableFactory(mock(DefaultSensorContext.class));
+ BatchComponent component = new BatchComponent(1, File.create("foo/bar.c").setEffectiveKey("foo/bar.c"), null);
+ Issuable issuable = factory.loadPerspective(Issuable.class, component);
+
+ assertThat(issuable).isNotNull();
+ assertThat(issuable.issues()).isEmpty();
+ }
+
+ @Test
+ public void project_should_be_issuable() {
+ IssuableFactory factory = new IssuableFactory(mock(DefaultSensorContext.class));
+ BatchComponent component = new BatchComponent(1, new Project("Foo").setEffectiveKey("foo"), null);
+ Issuable issuable = factory.loadPerspective(Issuable.class, component);
+
+ assertThat(issuable).isNotNull();
+ assertThat(issuable.issues()).isEmpty();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssueCacheTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssueCacheTest.java
new file mode 100644
index 00000000000..6d9f42a8ede
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/IssueCacheTest.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import org.sonar.batch.index.AbstractCachesTest;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.sonar.api.rule.Severity;
+
+import javax.annotation.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueCacheTest extends AbstractCachesTest {
+
+ @Test
+ public void should_add_new_issue() {
+ IssueCache cache = new IssueCache(caches);
+ TrackedIssue issue1 = createIssue("111", "org.struts.Action", null);
+ TrackedIssue issue2 = createIssue("222", "org.struts.Action", null);
+ TrackedIssue issue3 = createIssue("333", "org.struts.Filter", null);
+ issue3.setAssignee("foo");
+ cache.put(issue1).put(issue2).put(issue3);
+
+ assertThat(issueKeys(cache.byComponent("org.struts.Action"))).containsOnly("111", "222");
+ assertThat(issueKeys(cache.byComponent("org.struts.Filter"))).containsOnly("333");
+ assertThat(cache.byComponent("org.struts.Filter").iterator().next().assignee()).isEqualTo("foo");
+ }
+
+ @Test
+ public void should_update_existing_issue() {
+ IssueCache cache = new IssueCache(caches);
+ TrackedIssue issue = createIssue("111", "org.struts.Action", Severity.BLOCKER);
+ cache.put(issue);
+
+ issue.setSeverity(Severity.MINOR);
+ cache.put(issue);
+
+ List<TrackedIssue> issues = ImmutableList.copyOf(cache.byComponent("org.struts.Action"));
+ assertThat(issues).hasSize(1);
+ TrackedIssue reloaded = issues.iterator().next();
+ assertThat(reloaded.key()).isEqualTo("111");
+ assertThat(reloaded.severity()).isEqualTo(Severity.MINOR);
+ }
+
+ @Test
+ public void should_get_all_issues() {
+ IssueCache cache = new IssueCache(caches);
+ TrackedIssue issue1 = createIssue("111", "org.struts.Action", Severity.BLOCKER);
+ TrackedIssue issue2 = createIssue("222", "org.struts.Filter", Severity.INFO);
+ cache.put(issue1).put(issue2);
+
+ List<TrackedIssue> issues = ImmutableList.copyOf(cache.all());
+ assertThat(issues).containsOnly(issue1, issue2);
+ }
+
+ private Collection<String> issueKeys(Iterable<TrackedIssue> issues) {
+ return Collections2.transform(ImmutableList.copyOf(issues), new Function<TrackedIssue, String>() {
+ @Override
+ public String apply(@Nullable TrackedIssue issue) {
+ return issue.key();
+ }
+ });
+ }
+
+ private TrackedIssue createIssue(String key, String componentKey, String severity) {
+ TrackedIssue issue = new TrackedIssue();
+ issue.setKey(key);
+ issue.setComponentKey(componentKey);
+ issue.setSeverity(severity);
+
+ return issue;
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ModuleIssuesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ModuleIssuesTest.java
new file mode 100644
index 00000000000..af84563a8b8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ModuleIssuesTest.java
@@ -0,0 +1,221 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.io.StringReader;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.batch.rule.internal.RulesBuilder;
+import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
+import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation;
+import org.sonar.api.resources.File;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.scanner.protocol.output.ScannerReport;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ModuleIssuesTest {
+
+ static final RuleKey SQUID_RULE_KEY = RuleKey.of("squid", "AvoidCycle");
+ static final String SQUID_RULE_NAME = "Avoid Cycle";
+
+ @Mock
+ IssueFilters filters;
+
+ ActiveRulesBuilder activeRulesBuilder = new ActiveRulesBuilder();
+ RulesBuilder ruleBuilder = new RulesBuilder();
+
+ ModuleIssues moduleIssues;
+
+ BatchComponentCache componentCache = new BatchComponentCache();
+ InputFile file = new DefaultInputFile("foo", "src/Foo.php").initMetadata(new FileMetadata().readMetadata(new StringReader("Foo\nBar\nBiz\n")));
+ ReportPublisher reportPublisher = mock(ReportPublisher.class, RETURNS_DEEP_STUBS);
+
+ @Before
+ public void prepare() {
+ componentCache.add(File.create("src/Foo.php").setEffectiveKey("foo:src/Foo.php"), null).setInputComponent(file);
+ }
+
+ @Test
+ public void fail_on_unknown_rule() {
+ initModuleIssues();
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo"))
+ .forRule(SQUID_RULE_KEY);
+ try {
+ moduleIssues.initAndAddIssue(issue);
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(MessageException.class);
+ }
+
+ verifyZeroInteractions(reportPublisher);
+ }
+
+ @Test
+ public void fail_if_rule_has_no_name_and_issue_has_no_message() {
+ ruleBuilder.add(SQUID_RULE_KEY).setInternalKey(SQUID_RULE_KEY.rule());
+ initModuleIssues();
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message(""))
+ .forRule(SQUID_RULE_KEY);
+ try {
+ moduleIssues.initAndAddIssue(issue);
+ fail();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(MessageException.class);
+ }
+
+ verifyZeroInteractions(reportPublisher);
+ }
+
+ @Test
+ public void ignore_null_active_rule() {
+ ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
+ initModuleIssues();
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo"))
+ .forRule(SQUID_RULE_KEY);
+ boolean added = moduleIssues.initAndAddIssue(issue);
+
+ assertThat(added).isFalse();
+ verifyZeroInteractions(reportPublisher);
+ }
+
+ @Test
+ public void ignore_null_rule_of_active_rule() {
+ ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
+ activeRulesBuilder.create(SQUID_RULE_KEY).activate();
+ initModuleIssues();
+
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo"))
+ .forRule(SQUID_RULE_KEY);
+ boolean added = moduleIssues.initAndAddIssue(issue);
+
+ assertThat(added).isFalse();
+ verifyZeroInteractions(reportPublisher);
+ }
+
+ @Test
+ public void add_issue_to_cache() {
+ ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
+ activeRulesBuilder.create(SQUID_RULE_KEY).setSeverity(Severity.INFO).activate();
+ initModuleIssues();
+
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo"))
+ .forRule(SQUID_RULE_KEY)
+ .overrideSeverity(org.sonar.api.batch.rule.Severity.CRITICAL);
+
+ when(filters.accept(anyString(), any(ScannerReport.Issue.class))).thenReturn(true);
+
+ boolean added = moduleIssues.initAndAddIssue(issue);
+
+ assertThat(added).isTrue();
+ ArgumentCaptor<ScannerReport.Issue> argument = ArgumentCaptor.forClass(ScannerReport.Issue.class);
+ verify(reportPublisher.getWriter()).appendComponentIssue(eq(1), argument.capture());
+ assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL);
+ }
+
+ @Test
+ public void use_severity_from_active_rule_if_no_severity_on_issue() {
+ ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
+ activeRulesBuilder.create(SQUID_RULE_KEY).setSeverity(Severity.INFO).activate();
+ initModuleIssues();
+
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo"))
+ .forRule(SQUID_RULE_KEY);
+ when(filters.accept(anyString(), any(ScannerReport.Issue.class))).thenReturn(true);
+ moduleIssues.initAndAddIssue(issue);
+
+ ArgumentCaptor<ScannerReport.Issue> argument = ArgumentCaptor.forClass(ScannerReport.Issue.class);
+ verify(reportPublisher.getWriter()).appendComponentIssue(eq(1), argument.capture());
+ assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.INFO);
+ }
+
+ @Test
+ public void use_rule_name_if_no_message() {
+ ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
+ activeRulesBuilder.create(SQUID_RULE_KEY).setSeverity(Severity.INFO).setName(SQUID_RULE_NAME).activate();
+ initModuleIssues();
+
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message(""))
+ .forRule(SQUID_RULE_KEY);
+ when(filters.accept(anyString(), any(ScannerReport.Issue.class))).thenReturn(true);
+
+ boolean added = moduleIssues.initAndAddIssue(issue);
+
+ assertThat(added).isTrue();
+ ArgumentCaptor<ScannerReport.Issue> argument = ArgumentCaptor.forClass(ScannerReport.Issue.class);
+ verify(reportPublisher.getWriter()).appendComponentIssue(eq(1), argument.capture());
+ assertThat(argument.getValue().getMsg()).isEqualTo("Avoid Cycle");
+ }
+
+ @Test
+ public void filter_issue() {
+ ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME);
+ activeRulesBuilder.create(SQUID_RULE_KEY).setSeverity(Severity.INFO).activate();
+ initModuleIssues();
+
+ DefaultIssue issue = new DefaultIssue()
+ .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message(""))
+ .forRule(SQUID_RULE_KEY);
+
+ when(filters.accept(anyString(), any(ScannerReport.Issue.class))).thenReturn(false);
+
+ boolean added = moduleIssues.initAndAddIssue(issue);
+
+ assertThat(added).isFalse();
+ verifyZeroInteractions(reportPublisher);
+ }
+
+ /**
+ * Every rules and active rules has to be added in builders before creating ModuleIssues
+ */
+ private void initModuleIssues() {
+ moduleIssues = new ModuleIssues(activeRulesBuilder.build(), ruleBuilder.build(), filters, reportPublisher, componentCache);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/TrackedIssueAdapterTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/TrackedIssueAdapterTest.java
new file mode 100644
index 00000000000..11fc560b318
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/TrackedIssueAdapterTest.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue;
+
+import java.util.Date;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TrackedIssueAdapterTest {
+
+ @Test
+ public void improve_coverage() {
+ Date creationDate = new Date();
+ TrackedIssue trackedIssue = new TrackedIssue()
+ .setKey("XYZ123")
+ .setComponentKey("foo")
+ .setRuleKey(RuleKey.of("repo", "rule"))
+ .setSeverity("MAJOR")
+ .setMessage("msg")
+ .setStartLine(1)
+ .setGap(2.0)
+ .setStatus("RESOLVED")
+ .setResolution("FIXED")
+ .setReporter("toto")
+ .setAssignee("tata")
+ .setNew(true)
+ .setCreationDate(creationDate);
+ Issue issue = new TrackedIssueAdapter(trackedIssue);
+ assertThat(issue.key()).isEqualTo("XYZ123");
+ assertThat(issue.componentKey()).isEqualTo("foo");
+ assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule"));
+ assertThat(issue.severity()).isEqualTo("MAJOR");
+ assertThat(issue.message()).isEqualTo("msg");
+ assertThat(issue.line()).isEqualTo(1);
+ assertThat(issue.effortToFix()).isEqualTo(2.0);
+ assertThat(issue.status()).isEqualTo("RESOLVED");
+ assertThat(issue.resolution()).isEqualTo("FIXED");
+ assertThat(issue.reporter()).isEqualTo("toto");
+ assertThat(issue.assignee()).isEqualTo("tata");
+ assertThat(issue.isNew()).isTrue();
+ assertThat(issue.attribute("foo")).isNull();
+ assertThat(issue.creationDate()).isEqualTo(creationDate);
+ assertThat(issue.language()).isNull();
+ assertThat(issue.updateDate()).isNull();
+ assertThat(issue.closeDate()).isNull();
+ assertThat(issue.authorLogin()).isNull();
+ assertThat(issue.actionPlanKey()).isNull();
+ assertThat(issue.comments()).isEmpty();
+ assertThat(issue.debt()).isNull();
+ assertThat(issue.projectKey()).isNull();
+ assertThat(issue.projectUuid()).isNull();
+ assertThat(issue.componentUuid()).isNull();
+ assertThat(issue.tags()).isEmpty();
+
+ assertThat(issue).isNotEqualTo(null);
+ assertThat(issue).isNotEqualTo("Foo");
+ assertThat(issue).isEqualTo(new TrackedIssueAdapter(trackedIssue));
+ assertThat(issue.hashCode()).isEqualTo(trackedIssue.key().hashCode());
+ assertThat(issue).isNotEqualTo(new TrackedIssueAdapter(new TrackedIssue()
+ .setKey("another")));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/EnforceIssuesFilterTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/EnforceIssuesFilterTest.java
new file mode 100644
index 00000000000..28df728657d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/EnforceIssuesFilterTest.java
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.scan.issue.filter.IssueFilterChain;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.WildcardPattern;
+import org.sonar.batch.issue.ignore.pattern.IssueInclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssuePattern;
+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 EnforceIssuesFilterTest {
+
+ private IssueInclusionPatternInitializer exclusionPatternInitializer;
+ private EnforceIssuesFilter ignoreFilter;
+ private FilterableIssue issue;
+ private IssueFilterChain chain;
+
+ @Before
+ public void init() {
+ exclusionPatternInitializer = mock(IssueInclusionPatternInitializer.class);
+ issue = mock(FilterableIssue.class);
+ chain = mock(IssueFilterChain.class);
+ when(chain.accept(issue)).thenReturn(true);
+
+ ignoreFilter = new EnforceIssuesFilter(exclusionPatternInitializer);
+ }
+
+ @Test
+ public void shouldPassToChainIfNoConfiguredPatterns() {
+ assertThat(ignoreFilter.accept(issue, chain)).isTrue();
+ verify(chain).accept(issue);
+ }
+
+ @Test
+ public void shouldPassToChainIfRuleDoesNotMatch() {
+ String rule = "rule";
+ RuleKey ruleKey = mock(RuleKey.class);
+ when(ruleKey.toString()).thenReturn(rule);
+ when(issue.ruleKey()).thenReturn(ruleKey);
+
+ IssuePattern matching = mock(IssuePattern.class);
+ WildcardPattern rulePattern = mock(WildcardPattern.class);
+ when(matching.getRulePattern()).thenReturn(rulePattern);
+ when(rulePattern.match(rule)).thenReturn(false);
+ when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(ImmutableList.of(matching));
+
+ assertThat(ignoreFilter.accept(issue, chain)).isTrue();
+ verify(chain).accept(issue);
+ }
+
+ @Test
+ public void shouldAcceptIssueIfFullyMatched() {
+ String rule = "rule";
+ String path = "org/sonar/api/Issue.java";
+ String componentKey = "org.sonar.api.Issue";
+ RuleKey ruleKey = mock(RuleKey.class);
+ when(ruleKey.toString()).thenReturn(rule);
+ when(issue.ruleKey()).thenReturn(ruleKey);
+ when(issue.componentKey()).thenReturn(componentKey);
+
+ IssuePattern matching = mock(IssuePattern.class);
+ WildcardPattern rulePattern = mock(WildcardPattern.class);
+ when(matching.getRulePattern()).thenReturn(rulePattern);
+ when(rulePattern.match(rule)).thenReturn(true);
+ WildcardPattern pathPattern = mock(WildcardPattern.class);
+ when(matching.getResourcePattern()).thenReturn(pathPattern);
+ when(pathPattern.match(path)).thenReturn(true);
+ when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(ImmutableList.of(matching));
+ when(exclusionPatternInitializer.getPathForComponent(componentKey)).thenReturn(path);
+
+ assertThat(ignoreFilter.accept(issue, chain)).isTrue();
+ verifyZeroInteractions(chain);
+ }
+
+ @Test
+ public void shouldRefuseIssueIfRuleMatchesButNotPath() {
+ String rule = "rule";
+ String path = "org/sonar/api/Issue.java";
+ String componentKey = "org.sonar.api.Issue";
+ RuleKey ruleKey = mock(RuleKey.class);
+ when(ruleKey.toString()).thenReturn(rule);
+ when(issue.ruleKey()).thenReturn(ruleKey);
+ when(issue.componentKey()).thenReturn(componentKey);
+
+ IssuePattern matching = mock(IssuePattern.class);
+ WildcardPattern rulePattern = mock(WildcardPattern.class);
+ when(matching.getRulePattern()).thenReturn(rulePattern);
+ when(rulePattern.match(rule)).thenReturn(true);
+ WildcardPattern pathPattern = mock(WildcardPattern.class);
+ when(matching.getResourcePattern()).thenReturn(pathPattern);
+ when(pathPattern.match(path)).thenReturn(false);
+ when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(ImmutableList.of(matching));
+ when(exclusionPatternInitializer.getPathForComponent(componentKey)).thenReturn(path);
+
+ assertThat(ignoreFilter.accept(issue, chain)).isFalse();
+ verifyZeroInteractions(chain);
+ }
+
+ @Test
+ public void shouldRefuseIssueIfRuleMatchesAndPathUnknown() {
+ String rule = "rule";
+ String path = "org/sonar/api/Issue.java";
+ String componentKey = "org.sonar.api.Issue";
+ RuleKey ruleKey = mock(RuleKey.class);
+ when(ruleKey.toString()).thenReturn(rule);
+ when(issue.ruleKey()).thenReturn(ruleKey);
+ when(issue.componentKey()).thenReturn(componentKey);
+
+ IssuePattern matching = mock(IssuePattern.class);
+ WildcardPattern rulePattern = mock(WildcardPattern.class);
+ when(matching.getRulePattern()).thenReturn(rulePattern);
+ when(rulePattern.match(rule)).thenReturn(true);
+ WildcardPattern pathPattern = mock(WildcardPattern.class);
+ when(matching.getResourcePattern()).thenReturn(pathPattern);
+ when(pathPattern.match(path)).thenReturn(false);
+ when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(ImmutableList.of(matching));
+ when(exclusionPatternInitializer.getPathForComponent(componentKey)).thenReturn(null);
+
+ assertThat(ignoreFilter.accept(issue, chain)).isFalse();
+ verifyZeroInteractions(chain);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilterTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilterTest.java
new file mode 100644
index 00000000000..6cf431e215c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/IgnoreIssuesFilterTest.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.scan.issue.filter.IssueFilterChain;
+import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssuePattern;
+import org.sonar.batch.issue.ignore.pattern.PatternMatcher;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class IgnoreIssuesFilterTest {
+
+ private IssueExclusionPatternInitializer exclusionPatternInitializer;
+ private PatternMatcher exclusionPatternMatcher;
+ private IgnoreIssuesFilter ignoreFilter;
+ private FilterableIssue issue;
+ private IssueFilterChain chain;
+
+ @Before
+ public void init() {
+ exclusionPatternMatcher = mock(PatternMatcher.class);
+ exclusionPatternInitializer = mock(IssueExclusionPatternInitializer.class);
+ when(exclusionPatternInitializer.getPatternMatcher()).thenReturn(exclusionPatternMatcher);
+ issue = mock(FilterableIssue.class);
+ chain = mock(IssueFilterChain.class);
+ when(chain.accept(issue)).thenReturn(true);
+
+ ignoreFilter = new IgnoreIssuesFilter(exclusionPatternInitializer);
+ }
+
+ @Test
+ public void shouldPassToChainIfMatcherHasNoPatternForIssue() {
+ when(exclusionPatternMatcher.getMatchingPattern(issue)).thenReturn(null);
+
+ assertThat(ignoreFilter.accept(issue, chain)).isTrue();
+ }
+
+ @Test
+ public void shouldAcceptOrRefuseIfMatcherHasPatternForIssue() {
+ when(exclusionPatternMatcher.getMatchingPattern(issue)).thenReturn(mock(IssuePattern.class));
+
+ assertThat(ignoreFilter.accept(issue, chain)).isFalse();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializerTest.java
new file mode 100644
index 00000000000..13e1646bb9d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueExclusionPatternInitializerTest.java
@@ -0,0 +1,141 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.SonarException;
+import org.sonar.core.config.IssueExclusionProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueExclusionPatternInitializerTest {
+
+ private IssueExclusionPatternInitializer patternsInitializer;
+
+ private Settings settings;
+
+ @Before
+ public void init() {
+ settings = new Settings(new PropertyDefinitions(IssueExclusionProperties.all()));
+ patternsInitializer = new IssueExclusionPatternInitializer(settings);
+ }
+
+ @Test
+ public void testNoConfiguration() {
+ patternsInitializer.initPatterns();
+ assertThat(patternsInitializer.hasConfiguredPatterns()).isFalse();
+ assertThat(patternsInitializer.getMulticriteriaPatterns().size()).isEqualTo(0);
+ }
+
+ @Test
+ public void shouldHavePatternsBasedOnMulticriteriaPattern() {
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria", "1,2");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".1." + "resourceKey", "org/foo/Bar.java");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".1." + "ruleKey", "*");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".2." + "resourceKey", "org/foo/Hello.java");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".2." + "ruleKey", "checkstyle:MagicNumber");
+ patternsInitializer.initPatterns();
+
+ assertThat(patternsInitializer.hasConfiguredPatterns()).isTrue();
+ assertThat(patternsInitializer.hasFileContentPattern()).isFalse();
+ assertThat(patternsInitializer.hasMulticriteriaPatterns()).isTrue();
+ assertThat(patternsInitializer.getMulticriteriaPatterns().size()).isEqualTo(2);
+ assertThat(patternsInitializer.getBlockPatterns().size()).isEqualTo(0);
+ assertThat(patternsInitializer.getAllFilePatterns().size()).isEqualTo(0);
+
+ patternsInitializer.initializePatternsForPath("org/foo/Bar.java", "org.foo.Bar");
+ patternsInitializer.initializePatternsForPath("org/foo/Baz.java", "org.foo.Baz");
+ patternsInitializer.initializePatternsForPath("org/foo/Hello.java", "org.foo.Hello");
+
+ assertThat(patternsInitializer.getPatternMatcher().getPatternsForComponent("org.foo.Bar")).hasSize(1);
+ assertThat(patternsInitializer.getPatternMatcher().getPatternsForComponent("org.foo.Baz")).hasSize(0);
+ assertThat(patternsInitializer.getPatternMatcher().getPatternsForComponent("org.foo.Hello")).hasSize(1);
+
+ }
+
+ @Test(expected = SonarException.class)
+ public void shouldLogInvalidResourceKey() {
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria", "1");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".1." + "resourceKey", "");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".1." + "ruleKey", "*");
+ patternsInitializer.initPatterns();
+ }
+
+ @Test(expected = SonarException.class)
+ public void shouldLogInvalidRuleKey() {
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria", "1");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".1." + "resourceKey", "*");
+ settings.setProperty("sonar.issue.ignore" + ".multicriteria" + ".1." + "ruleKey", "");
+ patternsInitializer.initPatterns();
+ }
+
+ @Test
+ public void shouldReturnBlockPattern() {
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY, "1,2,3");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionProperties.BEGIN_BLOCK_REGEXP, "// SONAR-OFF");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionProperties.END_BLOCK_REGEXP, "// SONAR-ON");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".2." + IssueExclusionProperties.BEGIN_BLOCK_REGEXP, "// FOO-OFF");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".2." + IssueExclusionProperties.END_BLOCK_REGEXP, "// FOO-ON");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".3." + IssueExclusionProperties.BEGIN_BLOCK_REGEXP, "// IGNORE-TO-EOF");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".3." + IssueExclusionProperties.END_BLOCK_REGEXP, "");
+ patternsInitializer.loadFileContentPatterns();
+
+ assertThat(patternsInitializer.hasConfiguredPatterns()).isTrue();
+ assertThat(patternsInitializer.hasFileContentPattern()).isTrue();
+ assertThat(patternsInitializer.hasMulticriteriaPatterns()).isFalse();
+ assertThat(patternsInitializer.getMulticriteriaPatterns().size()).isEqualTo(0);
+ assertThat(patternsInitializer.getBlockPatterns().size()).isEqualTo(3);
+ assertThat(patternsInitializer.getAllFilePatterns().size()).isEqualTo(0);
+ }
+
+ @Test(expected = SonarException.class)
+ public void shouldLogInvalidStartBlockPattern() {
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY, "1");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionProperties.BEGIN_BLOCK_REGEXP, "");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionProperties.END_BLOCK_REGEXP, "// SONAR-ON");
+ patternsInitializer.loadFileContentPatterns();
+ }
+
+ @Test
+ public void shouldReturnAllFilePattern() {
+ settings.setProperty(IssueExclusionProperties.PATTERNS_ALLFILE_KEY, "1,2");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_ALLFILE_KEY + ".1." + IssueExclusionProperties.FILE_REGEXP, "@SONAR-IGNORE-ALL");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_ALLFILE_KEY + ".2." + IssueExclusionProperties.FILE_REGEXP, "//FOO-IGNORE-ALL");
+ patternsInitializer.loadFileContentPatterns();
+
+ assertThat(patternsInitializer.hasConfiguredPatterns()).isTrue();
+ assertThat(patternsInitializer.hasFileContentPattern()).isTrue();
+ assertThat(patternsInitializer.hasMulticriteriaPatterns()).isFalse();
+ assertThat(patternsInitializer.getMulticriteriaPatterns().size()).isEqualTo(0);
+ assertThat(patternsInitializer.getBlockPatterns().size()).isEqualTo(0);
+ assertThat(patternsInitializer.getAllFilePatterns().size()).isEqualTo(2);
+ }
+
+ @Test(expected = SonarException.class)
+ public void shouldLogInvalidAllFilePattern() {
+ settings.setProperty(IssueExclusionProperties.PATTERNS_ALLFILE_KEY, "1");
+ settings.setProperty(IssueExclusionProperties.PATTERNS_ALLFILE_KEY + ".1." + IssueExclusionProperties.FILE_REGEXP, "");
+ patternsInitializer.loadFileContentPatterns();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializerTest.java
new file mode 100644
index 00000000000..25c74587522
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssueInclusionPatternInitializerTest.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.core.config.IssueExclusionProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueInclusionPatternInitializerTest {
+
+ private IssueInclusionPatternInitializer patternsInitializer;
+
+ private Settings settings;
+
+ @Before
+ public void init() {
+ settings = new Settings(new PropertyDefinitions(IssueExclusionProperties.all()));
+ patternsInitializer = new IssueInclusionPatternInitializer(settings);
+ }
+
+ @Test
+ public void testNoConfiguration() {
+ patternsInitializer.initPatterns();
+ assertThat(patternsInitializer.hasConfiguredPatterns()).isFalse();
+ }
+
+ @Test
+ public void shouldHavePatternsBasedOnMulticriteriaPattern() {
+ settings.setProperty("sonar.issue.enforce" + ".multicriteria", "1,2");
+ settings.setProperty("sonar.issue.enforce" + ".multicriteria" + ".1." + "resourceKey", "org/foo/Bar.java");
+ settings.setProperty("sonar.issue.enforce" + ".multicriteria" + ".1." + "ruleKey", "*");
+ settings.setProperty("sonar.issue.enforce" + ".multicriteria" + ".2." + "resourceKey", "org/foo/Hello.java");
+ settings.setProperty("sonar.issue.enforce" + ".multicriteria" + ".2." + "ruleKey", "checkstyle:MagicNumber");
+ patternsInitializer.initPatterns();
+
+ assertThat(patternsInitializer.hasConfiguredPatterns()).isTrue();
+ assertThat(patternsInitializer.hasMulticriteriaPatterns()).isTrue();
+ assertThat(patternsInitializer.getMulticriteriaPatterns().size()).isEqualTo(2);
+
+ patternsInitializer.initializePatternsForPath("org/foo/Bar.java", "org.foo.Bar");
+ patternsInitializer.initializePatternsForPath("org/foo/Baz.java", "org.foo.Baz");
+ patternsInitializer.initializePatternsForPath("org/foo/Hello.java", "org.foo.Hello");
+
+ assertThat(patternsInitializer.getPathForComponent("org.foo.Bar")).isEqualTo("org/foo/Bar.java");
+ assertThat(patternsInitializer.getPathForComponent("org.foo.Baz")).isEqualTo("org/foo/Baz.java");
+ assertThat(patternsInitializer.getPathForComponent("org.foo.Hello")).isEqualTo("org/foo/Hello.java");
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssuePatternTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssuePatternTest.java
new file mode 100644
index 00000000000..e67111d0f9c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/IssuePatternTest.java
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class IssuePatternTest {
+
+ @Test
+ public void shouldMatchLines() {
+ IssuePattern pattern = new IssuePattern("*", "*");
+ pattern.addLine(12).addLine(15).addLineRange(20, 25);
+
+ assertThat(pattern.matchLine(3)).isFalse();
+ assertThat(pattern.matchLine(12)).isTrue();
+ assertThat(pattern.matchLine(14)).isFalse();
+ assertThat(pattern.matchLine(21)).isTrue();
+ assertThat(pattern.matchLine(6599)).isFalse();
+ }
+
+ @Test
+ public void shouldMatchJavaFile() {
+ String javaFile = "org.foo.Bar";
+ assertThat(new IssuePattern("org.foo.Bar", "*").matchResource(javaFile)).isTrue();
+ assertThat(new IssuePattern("org.foo.*", "*").matchResource(javaFile)).isTrue();
+ assertThat(new IssuePattern("*Bar", "*").matchResource(javaFile)).isTrue();
+ assertThat(new IssuePattern("*", "*").matchResource(javaFile)).isTrue();
+ assertThat(new IssuePattern("org.*.?ar", "*").matchResource(javaFile)).isTrue();
+
+ assertThat(new IssuePattern("org.other.Hello", "*").matchResource(javaFile)).isFalse();
+ assertThat(new IssuePattern("org.foo.Hello", "*").matchResource(javaFile)).isFalse();
+ assertThat(new IssuePattern("org.*.??ar", "*").matchResource(javaFile)).isFalse();
+ assertThat(new IssuePattern("org.*.??ar", "*").matchResource(null)).isFalse();
+ assertThat(new IssuePattern("org.*.??ar", "*").matchResource("plop")).isFalse();
+ }
+
+ @Test
+ public void shouldMatchRule() {
+ RuleKey rule = Rule.create("checkstyle", "IllegalRegexp", "").ruleKey();
+ assertThat(new IssuePattern("*", "*").matchRule(rule)).isTrue();
+ assertThat(new IssuePattern("*", "checkstyle:*").matchRule(rule)).isTrue();
+ assertThat(new IssuePattern("*", "checkstyle:IllegalRegexp").matchRule(rule)).isTrue();
+ assertThat(new IssuePattern("*", "checkstyle:Illegal*").matchRule(rule)).isTrue();
+ assertThat(new IssuePattern("*", "*:*Illegal*").matchRule(rule)).isTrue();
+
+ assertThat(new IssuePattern("*", "pmd:IllegalRegexp").matchRule(rule)).isFalse();
+ assertThat(new IssuePattern("*", "pmd:*").matchRule(rule)).isFalse();
+ assertThat(new IssuePattern("*", "*:Foo*IllegalRegexp").matchRule(rule)).isFalse();
+ }
+
+ @Test
+ public void shouldMatchViolation() {
+ Rule rule = Rule.create("checkstyle", "IllegalRegexp", "");
+ String javaFile = "org.foo.Bar";
+
+ IssuePattern pattern = new IssuePattern("*", "*");
+ pattern.addLine(12);
+
+ assertThat(pattern.match(create(rule, javaFile, null))).isFalse();
+ assertThat(pattern.match(create(rule, javaFile, 12))).isTrue();
+ assertThat(pattern.match(create((Rule) null, javaFile, 5))).isFalse();
+ assertThat(pattern.match(create(rule, null, null))).isFalse();
+ assertThat(pattern.match(create((Rule) null, null, null))).isFalse();
+ }
+
+ private FilterableIssue create(Rule rule, String component, Integer line) {
+ FilterableIssue mockIssue = mock(FilterableIssue.class);
+ RuleKey ruleKey = null;
+ if (rule != null) {
+ ruleKey = rule.ruleKey();
+ }
+ when(mockIssue.ruleKey()).thenReturn(ruleKey);
+ when(mockIssue.componentKey()).thenReturn(component);
+ when(mockIssue.line()).thenReturn(line);
+ return mockIssue;
+ }
+
+ @Test
+ public void shouldNotMatchNullRule() {
+ assertThat(new IssuePattern("*", "*").matchRule(null)).isFalse();
+ }
+
+ @Test
+ public void shouldPrintPatternToString() {
+ IssuePattern pattern = new IssuePattern("*", "checkstyle:*");
+
+ assertThat(pattern.toString()).isEqualTo(
+ "IssuePattern[resourcePattern=*,rulePattern=checkstyle:*,lines=[],lineRanges=[],beginBlockRegexp=<null>,endBlockRegexp=<null>,allFileRegexp=<null>,checkLines=true]");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/LineRangeTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/LineRangeTest.java
new file mode 100644
index 00000000000..8af95b5d4ad
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/LineRangeTest.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LineRangeTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void lineRangeShouldBeOrdered() {
+ new LineRange(25, 12);
+ }
+
+ @Test
+ public void shouldConvertLineRangeToLines() {
+ LineRange range = new LineRange(12, 15);
+
+ assertThat(range.toLines()).containsOnly(12, 13, 14, 15);
+ }
+
+ @Test
+ public void shouldTestInclusionInRangeOfLines() {
+ LineRange range = new LineRange(12, 15);
+
+ assertThat(range.in(3)).isFalse();
+ assertThat(range.in(12)).isTrue();
+ assertThat(range.in(13)).isTrue();
+ assertThat(range.in(14)).isTrue();
+ assertThat(range.in(15)).isTrue();
+ assertThat(range.in(16)).isFalse();
+ }
+
+ @Test
+ public void testToString() throws Exception {
+ assertThat(new LineRange(12, 15).toString()).isEqualTo("[12-15]");
+ }
+
+ @Test
+ public void testEquals() throws Exception {
+ LineRange range = new LineRange(12, 15);
+ assertThat(range).isEqualTo(range);
+ assertThat(range).isEqualTo(new LineRange(12, 15));
+ assertThat(range).isNotEqualTo(new LineRange(12, 2000));
+ assertThat(range).isNotEqualTo(new LineRange(1000, 2000));
+ assertThat(range).isNotEqualTo(null);
+ assertThat(range).isNotEqualTo(new StringBuffer());
+ }
+
+ @Test
+ public void testHashCode() throws Exception {
+ assertThat(new LineRange(12, 15).hashCode()).isEqualTo(new LineRange(12, 15).hashCode());
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternDecoderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternDecoderTest.java
new file mode 100644
index 00000000000..3501d1a2326
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternDecoderTest.java
@@ -0,0 +1,187 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.SonarException;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PatternDecoderTest {
+
+ private PatternDecoder decoder = new PatternDecoder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void shouldReadString() {
+ String patternsList = "# a comment followed by a blank line\n\n" +
+ "# suppress all violations\n" +
+ "*;*;*\n\n" +
+ "# exclude a Java file\n" +
+ "com.foo.Bar;*;*\n\n" +
+ "# exclude a Java package\n" +
+ "com.foo.*;*;*\n\n" +
+ "# exclude a specific rule\n" +
+ "*;checkstyle:IllegalRegexp;*\n\n" +
+ "# exclude a specific rule on a specific file\n" +
+ "com.foo.Bar;checkstyle:IllegalRegexp;*\n";
+ List<IssuePattern> patterns = decoder.decode(patternsList);
+
+ assertThat(patterns).hasSize(5);
+ }
+
+ @Test
+ public void shouldCheckFormatOfResource() {
+ assertThat(PatternDecoder.isResource("")).isFalse();
+ assertThat(PatternDecoder.isResource("*")).isTrue();
+ assertThat(PatternDecoder.isResource("com.foo.*")).isTrue();
+ }
+
+ @Test
+ public void shouldCheckFormatOfRule() {
+ assertThat(PatternDecoder.isRule("")).isFalse();
+ assertThat(PatternDecoder.isRule("*")).isTrue();
+ assertThat(PatternDecoder.isRule("com.foo.*")).isTrue();
+ }
+
+ @Test
+ public void shouldCheckFormatOfLinesRange() {
+ assertThat(PatternDecoder.isLinesRange("")).isFalse();
+ assertThat(PatternDecoder.isLinesRange(" ")).isFalse();
+ assertThat(PatternDecoder.isLinesRange("12")).isFalse();
+ assertThat(PatternDecoder.isLinesRange("12,212")).isFalse();
+
+ assertThat(PatternDecoder.isLinesRange("*")).isTrue();
+ assertThat(PatternDecoder.isLinesRange("[]")).isTrue();
+ assertThat(PatternDecoder.isLinesRange("[13]")).isTrue();
+ assertThat(PatternDecoder.isLinesRange("[13,24]")).isTrue();
+ assertThat(PatternDecoder.isLinesRange("[13,24,25-500]")).isTrue();
+ assertThat(PatternDecoder.isLinesRange("[24-65]")).isTrue();
+ assertThat(PatternDecoder.isLinesRange("[13,24-65,84-89,122]")).isTrue();
+ }
+
+ @Test
+ public void shouldReadStarPatterns() {
+ IssuePattern pattern = decoder.decodeLine("*;*;*");
+
+ assertThat(pattern.getResourcePattern().toString()).isEqualTo("*");
+ assertThat(pattern.getRulePattern().toString()).isEqualTo("*");
+ assertThat(pattern.isCheckLines()).isFalse();
+ }
+
+ @Test
+ public void shouldReadLineIds() {
+ IssuePattern pattern = decoder.decodeLine("*;*;[10,25,98]");
+
+ assertThat(pattern.isCheckLines()).isTrue();
+ assertThat(pattern.getAllLines()).containsOnly(10, 25, 98);
+ }
+
+ @Test
+ public void shouldReadRangeOfLineIds() {
+ IssuePattern pattern = decoder.decodeLine("*;*;[10-12,25,97-100]");
+
+ assertThat(pattern.isCheckLines()).isTrue();
+ assertThat(pattern.getAllLines()).containsOnly(10, 11, 12, 25, 97, 98, 99, 100);
+ }
+
+ @Test
+ public void shouldNotExcludeLines() {
+ // [] is different than *
+ // - all violations are excluded on *
+ // * no violations are excluded on []
+ IssuePattern pattern = decoder.decodeLine("*;*;[]");
+
+ assertThat(pattern.isCheckLines()).isTrue();
+ assertThat(pattern.getAllLines()).isEmpty();
+ }
+
+ @Test
+ public void shouldReadBlockPattern() {
+ IssuePattern pattern = decoder.decodeLine("SONAR-OFF;SONAR-ON");
+
+ assertThat(pattern.getResourcePattern()).isNull();
+ assertThat(pattern.getBeginBlockRegexp()).isEqualTo("SONAR-OFF");
+ assertThat(pattern.getEndBlockRegexp()).isEqualTo("SONAR-ON");
+ }
+
+ @Test
+ public void shouldReadAllFilePattern() {
+ IssuePattern pattern = decoder.decodeLine("SONAR-ALL-OFF");
+
+ assertThat(pattern.getResourcePattern()).isNull();
+ assertThat(pattern.getAllFileRegexp()).isEqualTo("SONAR-ALL-OFF");
+ }
+
+ @Test
+ public void shouldFailToReadUncorrectLine1() {
+ thrown.expect(SonarException.class);
+ thrown.expectMessage("Exclusions > Issues : Invalid format. The following line has more than 3 fields separated by comma");
+
+ decoder.decode(";;;;");
+ }
+
+ @Test
+ public void shouldFailToReadUncorrectLine3() {
+ thrown.expect(SonarException.class);
+ thrown.expectMessage("Exclusions > Issues : Invalid format. The first field does not define a resource pattern");
+
+ decoder.decode(";*;*");
+ }
+
+ @Test
+ public void shouldFailToReadUncorrectLine4() {
+ thrown.expect(SonarException.class);
+ thrown.expectMessage("Exclusions > Issues : Invalid format. The second field does not define a rule pattern");
+
+ decoder.decode("*;;*");
+ }
+
+ @Test
+ public void shouldFailToReadUncorrectLine5() {
+ thrown.expect(SonarException.class);
+ thrown.expectMessage("Exclusions > Issues : Invalid format. The third field does not define a range of lines");
+
+ decoder.decode("*;*;blabla");
+ }
+
+ @Test
+ public void shouldFailToReadUncorrectLine6() {
+ thrown.expect(SonarException.class);
+ thrown.expectMessage("Exclusions > Issues : Invalid format. The first field does not define a regular expression");
+
+ decoder.decode(";ON");
+ }
+
+ @Test
+ public void shouldAcceptEmptyEndBlockRegexp() {
+ IssuePattern pattern = decoder.decodeLine("OFF;");
+
+ assertThat(pattern.getResourcePattern()).isNull();
+ assertThat(pattern.getBeginBlockRegexp()).isEqualTo("OFF");
+ assertThat(pattern.getEndBlockRegexp()).isEmpty();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternMatcherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternMatcherTest.java
new file mode 100644
index 00000000000..1d69a30c371
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/pattern/PatternMatcherTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.pattern;
+
+import org.sonar.api.scan.issue.filter.FilterableIssue;
+
+import com.google.common.collect.Sets;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class PatternMatcherTest {
+
+ public static final Rule CHECKSTYLE_RULE = Rule.create("checkstyle", "MagicNumber", "");
+ public static final String JAVA_FILE = "org.foo.Hello";
+
+ private PatternMatcher patternMatcher;
+
+ @Before
+ public void setUp() {
+ patternMatcher = new PatternMatcher();
+ }
+
+ @Test
+ public void shouldReturnExtraPatternForResource() {
+ String file = "foo";
+ patternMatcher.addPatternToExcludeResource(file);
+
+ IssuePattern extraPattern = patternMatcher.getPatternsForComponent(file).iterator().next();
+ assertThat(extraPattern.matchResource(file)).isTrue();
+ assertThat(extraPattern.isCheckLines()).isFalse();
+ }
+
+ @Test
+ public void shouldReturnExtraPatternForLinesOfResource() {
+ String file = "foo";
+ Set<LineRange> lineRanges = Sets.newHashSet();
+ lineRanges.add(new LineRange(25, 28));
+ patternMatcher.addPatternToExcludeLines(file, lineRanges);
+
+ IssuePattern extraPattern = patternMatcher.getPatternsForComponent(file).iterator().next();
+ assertThat(extraPattern.matchResource(file)).isTrue();
+ assertThat(extraPattern.getAllLines()).isEqualTo(Sets.newHashSet(25, 26, 27, 28));
+ }
+
+ @Test
+ public void shouldHaveNoMatcherIfNoneDefined() {
+ assertThat(patternMatcher.getMatchingPattern(create(CHECKSTYLE_RULE, JAVA_FILE, null))).isNull();
+ }
+
+ @Test
+ public void shouldMatchWithStandardPatterns() {
+ patternMatcher.addPatternForComponent(JAVA_FILE, createPattern("org.foo.Hello;checkstyle:MagicNumber;[15-200]"));
+
+ assertThat(patternMatcher.getMatchingPattern(create(CHECKSTYLE_RULE, JAVA_FILE, 150))).isNotNull();
+ }
+
+ @Test
+ public void shouldNotMatchWithStandardPatterns() {
+ patternMatcher.addPatternForComponent(JAVA_FILE, createPattern("org.foo.Hello;checkstyle:MagicNumber;[15-200]"));
+
+ assertThat(patternMatcher.getMatchingPattern(create(CHECKSTYLE_RULE, JAVA_FILE, 5))).isNull();
+ }
+
+ @Test
+ public void shouldMatchWithExtraPattern() {
+ patternMatcher.addPatternForComponent(JAVA_FILE, createPattern("org.foo.Hello;*;[15-200]"));
+
+ assertThat(patternMatcher.getMatchingPattern(create(CHECKSTYLE_RULE, JAVA_FILE, 150))).isNotNull();
+ }
+
+ @Test
+ public void shouldNotMatchWithExtraPattern() {
+ patternMatcher.addPatternForComponent(JAVA_FILE, createPattern("org.foo.Hello;*;[15-200]"));
+
+ assertThat(patternMatcher.getMatchingPattern(create(CHECKSTYLE_RULE, JAVA_FILE, 5))).isNull();
+ }
+
+ private FilterableIssue create(Rule rule, String component, Integer line) {
+ FilterableIssue mockIssue = mock(FilterableIssue.class);
+ RuleKey ruleKey = null;
+ if (rule != null) {
+ ruleKey = rule.ruleKey();
+ }
+ when(mockIssue.ruleKey()).thenReturn(ruleKey);
+ when(mockIssue.componentKey()).thenReturn(component);
+ when(mockIssue.line()).thenReturn(line);
+ return mockIssue;
+ }
+
+ private IssuePattern createPattern(String line) {
+ return new PatternDecoder().decode(line).get(0);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoaderTest.java
new file mode 100644
index 00000000000..52a0e59cbbe
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsLoaderTest.java
@@ -0,0 +1,157 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.scanner;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssueInclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.PatternMatcher;
+
+import java.io.File;
+import java.io.IOException;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class IssueExclusionsLoaderTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Mock
+ private IssueExclusionsRegexpScanner regexpScanner;
+
+ @Mock
+ private IssueInclusionPatternInitializer inclusionPatternInitializer;
+
+ @Mock
+ private IssueExclusionPatternInitializer exclusionPatternInitializer;
+
+ @Mock
+ private PatternMatcher patternMatcher;
+
+ private DefaultFileSystem fs;
+ private IssueExclusionsLoader scanner;
+ private File baseDir;
+
+ @Before
+ public void before() throws Exception {
+ baseDir = temp.newFolder();
+ fs = new DefaultFileSystem(baseDir.toPath()).setEncoding(UTF_8);
+ MockitoAnnotations.initMocks(this);
+ scanner = new IssueExclusionsLoader(regexpScanner, exclusionPatternInitializer, inclusionPatternInitializer, fs);
+ }
+
+ @Test
+ public void testToString() throws Exception {
+ assertThat(scanner.toString()).isEqualTo("Issues Exclusions - Source Scanner");
+ }
+
+ @Test
+ public void shouldExecute() {
+ when(exclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(true);
+ when(inclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(true);
+ assertThat(scanner.shouldExecuteOnProject(null)).isTrue();
+
+ when(exclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(true);
+ when(inclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(false);
+ assertThat(scanner.shouldExecuteOnProject(null)).isTrue();
+
+ when(exclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(false);
+ when(inclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(true);
+ assertThat(scanner.shouldExecuteOnProject(null)).isTrue();
+
+ when(exclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(false);
+ when(inclusionPatternInitializer.hasConfiguredPatterns()).thenReturn(false);
+ assertThat(scanner.shouldExecuteOnProject(null)).isFalse();
+
+ }
+
+ @Test
+ public void shouldAnalyzeProject() throws IOException {
+ File javaFile1 = new File(baseDir, "src/main/java/Foo.java");
+ fs.add(new DefaultInputFile("polop", "src/main/java/Foo.java")
+ .setType(InputFile.Type.MAIN));
+ File javaTestFile1 = new File(baseDir, "src/test/java/FooTest.java");
+ fs.add(new DefaultInputFile("polop", "src/test/java/FooTest.java")
+ .setType(InputFile.Type.TEST));
+
+ when(exclusionPatternInitializer.hasFileContentPattern()).thenReturn(true);
+
+ scanner.execute();
+
+ verify(inclusionPatternInitializer).initializePatternsForPath("src/main/java/Foo.java", "polop:src/main/java/Foo.java");
+ verify(inclusionPatternInitializer).initializePatternsForPath("src/test/java/FooTest.java", "polop:src/test/java/FooTest.java");
+ verify(exclusionPatternInitializer).initializePatternsForPath("src/main/java/Foo.java", "polop:src/main/java/Foo.java");
+ verify(exclusionPatternInitializer).initializePatternsForPath("src/test/java/FooTest.java", "polop:src/test/java/FooTest.java");
+ verify(regexpScanner).scan("polop:src/main/java/Foo.java", javaFile1, UTF_8);
+ verify(regexpScanner).scan("polop:src/test/java/FooTest.java", javaTestFile1, UTF_8);
+ }
+
+ @Test
+ public void shouldAnalyseFilesOnlyWhenRegexConfigured() {
+ File javaFile1 = new File(baseDir, "src/main/java/Foo.java");
+ fs.add(new DefaultInputFile("polop", "src/main/java/Foo.java")
+ .setType(InputFile.Type.MAIN));
+ File javaTestFile1 = new File(baseDir, "src/test/java/FooTest.java");
+ fs.add(new DefaultInputFile("polop", "src/test/java/FooTest.java")
+ .setType(InputFile.Type.TEST));
+ when(exclusionPatternInitializer.hasFileContentPattern()).thenReturn(false);
+
+ scanner.execute();
+
+ verify(inclusionPatternInitializer).initializePatternsForPath("src/main/java/Foo.java", "polop:src/main/java/Foo.java");
+ verify(inclusionPatternInitializer).initializePatternsForPath("src/test/java/FooTest.java", "polop:src/test/java/FooTest.java");
+ verify(exclusionPatternInitializer).initializePatternsForPath("src/main/java/Foo.java", "polop:src/main/java/Foo.java");
+ verify(exclusionPatternInitializer).initializePatternsForPath("src/test/java/FooTest.java", "polop:src/test/java/FooTest.java");
+ verifyZeroInteractions(regexpScanner);
+ }
+
+ @Test
+ public void shouldReportFailure() throws IOException {
+ File phpFile1 = new File(baseDir, "src/Foo.php");
+ fs.add(new DefaultInputFile("polop", "src/Foo.php")
+ .setType(InputFile.Type.MAIN));
+
+ when(exclusionPatternInitializer.hasFileContentPattern()).thenReturn(true);
+ doThrow(new IOException("BUG")).when(regexpScanner).scan("polop:src/Foo.php", phpFile1, UTF_8);
+
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Unable to read the source file");
+
+ scanner.execute();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest.java
new file mode 100644
index 00000000000..f4aa4ebd9f1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest.java
@@ -0,0 +1,168 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.ignore.scanner;
+
+import com.google.common.collect.Sets;
+import com.google.common.io.Resources;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
+import org.sonar.batch.issue.ignore.pattern.IssuePattern;
+import org.sonar.batch.issue.ignore.pattern.LineRange;
+import org.sonar.batch.issue.ignore.pattern.PatternMatcher;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Set;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class IssueExclusionsRegexpScannerTest {
+
+ private IssueExclusionsRegexpScanner regexpScanner;
+
+ private String javaFile;
+ @Mock
+ private IssueExclusionPatternInitializer patternsInitializer;
+ @Mock
+ private PatternMatcher patternMatcher;
+ @Mock
+ private IssuePattern allFilePattern;
+ @Mock
+ private IssuePattern blockPattern1;
+ @Mock
+ private IssuePattern blockPattern2;
+
+ @Before
+ public void init() {
+ MockitoAnnotations.initMocks(this);
+
+ when(allFilePattern.getAllFileRegexp()).thenReturn("@SONAR-IGNORE-ALL");
+ when(blockPattern1.getBeginBlockRegexp()).thenReturn("// SONAR-OFF");
+ when(blockPattern1.getEndBlockRegexp()).thenReturn("// SONAR-ON");
+ when(blockPattern2.getBeginBlockRegexp()).thenReturn("// FOO-OFF");
+ when(blockPattern2.getEndBlockRegexp()).thenReturn("// FOO-ON");
+ when(patternsInitializer.getAllFilePatterns()).thenReturn(Arrays.asList(allFilePattern));
+ when(patternsInitializer.getBlockPatterns()).thenReturn(Arrays.asList(blockPattern1, blockPattern2));
+ when(patternsInitializer.getPatternMatcher()).thenReturn(patternMatcher);
+
+ regexpScanner = new IssueExclusionsRegexpScanner(patternsInitializer);
+ verify(patternsInitializer, times(1)).getAllFilePatterns();
+ verify(patternsInitializer, times(1)).getBlockPatterns();
+
+ javaFile = "org.sonar.test.MyFile";
+ }
+
+ @Test
+ public void shouldDoNothing() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-no-regexp.txt").toURI()), UTF_8);
+
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+ @Test
+ public void shouldAddPatternToExcludeFile() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp.txt").toURI()), UTF_8);
+
+ verify(patternsInitializer).getPatternMatcher();
+ verify(patternMatcher, times(1)).addPatternToExcludeResource(javaFile);
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+ @Test
+ public void shouldAddPatternToExcludeFileEvenIfAlsoDoubleRegexps() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp-and-double-regexp.txt").toURI()), UTF_8);
+
+ verify(patternsInitializer).getPatternMatcher();
+ verify(patternMatcher, times(1)).addPatternToExcludeResource(javaFile);
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+ @Test
+ public void shouldAddPatternToExcludeLines() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp.txt").toURI()), UTF_8);
+
+ Set<LineRange> lineRanges = Sets.newHashSet();
+ lineRanges.add(new LineRange(21, 25));
+ verify(patternsInitializer).getPatternMatcher();
+ verify(patternMatcher, times(1)).addPatternToExcludeLines(javaFile, lineRanges);
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+ @Test
+ public void shouldAddPatternToExcludeLinesTillTheEnd() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-unfinished.txt").toURI()), UTF_8);
+
+ Set<LineRange> lineRanges = Sets.newHashSet();
+ lineRanges.add(new LineRange(21, 34));
+ verify(patternsInitializer).getPatternMatcher();
+ verify(patternMatcher, times(1)).addPatternToExcludeLines(javaFile, lineRanges);
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+ @Test
+ public void shouldAddPatternToExcludeSeveralLineRanges() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-twice.txt").toURI()), UTF_8);
+
+ Set<LineRange> lineRanges = Sets.newHashSet();
+ lineRanges.add(new LineRange(21, 25));
+ lineRanges.add(new LineRange(29, 33));
+ verify(patternsInitializer).getPatternMatcher();
+ verify(patternMatcher, times(1)).addPatternToExcludeLines(javaFile, lineRanges);
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+ @Test
+ public void shouldAddPatternToExcludeLinesWithWrongOrder() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-wrong-order.txt").toURI()), UTF_8);
+
+ Set<LineRange> lineRanges = Sets.newHashSet();
+ lineRanges.add(new LineRange(25, 35));
+ verify(patternsInitializer).getPatternMatcher();
+ verify(patternMatcher, times(1)).addPatternToExcludeLines(javaFile, lineRanges);
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+ @Test
+ public void shouldAddPatternToExcludeLinesWithMess() throws Exception {
+ regexpScanner.scan(javaFile, new File(Resources.getResource(
+ "org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-mess.txt").toURI()), UTF_8);
+
+ Set<LineRange> lineRanges = Sets.newHashSet();
+ lineRanges.add(new LineRange(21, 29));
+ verify(patternsInitializer).getPatternMatcher();
+ verify(patternMatcher, times(1)).addPatternToExcludeLines(javaFile, lineRanges);
+ verifyNoMoreInteractions(patternsInitializer);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoaderTest.java
new file mode 100644
index 00000000000..7d8ab1d2319
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/DefaultServerLineHashesLoaderTest.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.batch.cache.WSLoader.LoadStrategy;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.HttpDownloader;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.mockito.Matchers.any;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DefaultServerLineHashesLoaderTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void before() {
+ }
+
+ @Test
+ public void should_download_source_from_ws_if_preview_mode() {
+ WSLoader wsLoader = mock(WSLoader.class);
+ when(wsLoader.loadString(anyString(), any(LoadStrategy.class))).thenReturn(new WSLoaderResult<>("ae12\n\n43fb", true));
+
+ ServerLineHashesLoader lastSnapshots = new DefaultServerLineHashesLoader(wsLoader);
+
+ String[] hashes = lastSnapshots.getLineHashes("myproject:org/foo/Bar.c", null);
+ assertThat(hashes).containsOnly("ae12", "", "43fb");
+ verify(wsLoader).loadString("/api/sources/hash?key=myproject%3Aorg%2Ffoo%2FBar.c", LoadStrategy.CACHE_FIRST);
+ }
+
+ @Test
+ public void should_download_source_with_space_from_ws_if_preview_mode() {
+ WSLoader server = mock(WSLoader.class);
+ when(server.loadString(anyString(), any(LoadStrategy.class))).thenReturn(new WSLoaderResult<>("ae12\n\n43fb", true));
+
+ ServerLineHashesLoader lastSnapshots = new DefaultServerLineHashesLoader(server);
+
+ MutableBoolean fromCache = new MutableBoolean();
+ String[] hashes = lastSnapshots.getLineHashes("myproject:org/foo/Foo Bar.c", fromCache);
+ assertThat(fromCache.booleanValue()).isTrue();
+ assertThat(hashes).containsOnly("ae12", "", "43fb");
+ verify(server).loadString("/api/sources/hash?key=myproject%3Aorg%2Ffoo%2FFoo+Bar.c", LoadStrategy.CACHE_FIRST);
+ }
+
+ @Test
+ public void should_fail_to_download_source_from_ws() throws URISyntaxException {
+ WSLoader server = mock(WSLoader.class);
+ when(server.loadString(anyString(), any(LoadStrategy.class))).thenThrow(new HttpDownloader.HttpException(new URI(""), 500));
+
+ ServerLineHashesLoader lastSnapshots = new DefaultServerLineHashesLoader(server);
+
+ thrown.expect(HttpDownloader.HttpException.class);
+ lastSnapshots.getLineHashes("foo", null);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java
new file mode 100644
index 00000000000..8648fba48d1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.junit.Test;
+
+import static org.apache.commons.codec.digest.DigestUtils.md5Hex;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RollingFileHashesTest {
+
+ @Test
+ public void test_equals() {
+ RollingFileHashes a = RollingFileHashes.create(FileHashes.create(new String[] {md5Hex("line0"), md5Hex("line1"), md5Hex("line2")}), 1);
+ RollingFileHashes b = RollingFileHashes.create(FileHashes.create(new String[] {md5Hex("line0"), md5Hex("line1"), md5Hex("line2"), md5Hex("line3")}), 1);
+
+ assertThat(a.getHash(1) == b.getHash(1)).isTrue();
+ assertThat(a.getHash(2) == b.getHash(2)).isTrue();
+ assertThat(a.getHash(3) == b.getHash(3)).isFalse();
+
+ RollingFileHashes c = RollingFileHashes.create(FileHashes.create(new String[] {md5Hex("line-1"), md5Hex("line0"), md5Hex("line1"), md5Hex("line2"), md5Hex("line3")}), 1);
+ assertThat(a.getHash(1) == c.getHash(2)).isFalse();
+ assertThat(a.getHash(2) == c.getHash(3)).isTrue();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java
new file mode 100644
index 00000000000..9acf02b3577
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+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.mockito.Mockito;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+
+import static org.apache.commons.codec.digest.DigestUtils.md5Hex;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SourceHashHolderTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ SourceHashHolder sourceHashHolder;
+
+ ServerLineHashesLoader lastSnapshots;
+ DefaultInputFile file;
+
+ private File ioFile;
+
+ @Before
+ public void setUp() throws Exception {
+ lastSnapshots = mock(ServerLineHashesLoader.class);
+ file = mock(DefaultInputFile.class);
+ ioFile = temp.newFile();
+ when(file.file()).thenReturn(ioFile);
+ when(file.path()).thenReturn(ioFile.toPath());
+ when(file.lines()).thenReturn(1);
+ when(file.charset()).thenReturn(StandardCharsets.UTF_8);
+
+ sourceHashHolder = new SourceHashHolder(file, lastSnapshots);
+ }
+
+ @Test
+ public void should_lazy_load_line_hashes() throws Exception {
+ final String source = "source";
+ FileUtils.write(ioFile, source + "\n", StandardCharsets.UTF_8);
+ when(file.lines()).thenReturn(2);
+
+ assertThat(sourceHashHolder.getHashedSource().getHash(1)).isEqualTo(md5Hex(source));
+ assertThat(sourceHashHolder.getHashedSource().getHash(2)).isEqualTo("");
+ verify(file).key();
+ verify(file).status();
+
+ assertThat(sourceHashHolder.getHashedSource().getHash(1)).isEqualTo(md5Hex(source));
+ }
+
+ @Test
+ public void should_lazy_load_reference_hashes_when_status_changed() throws Exception {
+ final String source = "source";
+ String key = "foo:src/Foo.java";
+ FileUtils.write(ioFile, source, StandardCharsets.UTF_8);
+ when(file.key()).thenReturn(key);
+ when(file.status()).thenReturn(InputFile.Status.CHANGED);
+ when(lastSnapshots.getLineHashes(key, null)).thenReturn(new String[] {md5Hex(source)});
+
+ assertThat(sourceHashHolder.getHashedReference().getHash(1)).isEqualTo(md5Hex(source));
+ verify(lastSnapshots).getLineHashes(key, null);
+
+ assertThat(sourceHashHolder.getHashedReference().getHash(1)).isEqualTo(md5Hex(source));
+ Mockito.verifyNoMoreInteractions(lastSnapshots);
+ }
+
+ @Test
+ public void should_not_load_reference_hashes_when_status_same() throws Exception {
+ final String source = "source";
+ String key = "foo:src/Foo.java";
+ FileUtils.write(ioFile, source, StandardCharsets.UTF_8);
+ when(file.key()).thenReturn(key);
+ when(file.status()).thenReturn(InputFile.Status.SAME);
+
+ assertThat(sourceHashHolder.getHashedReference().getHash(1)).isEqualTo(md5Hex(source));
+ Mockito.verifyNoMoreInteractions(lastSnapshots);
+ }
+
+ @Test
+ public void no_reference_hashes_when_status_added() throws Exception {
+ final String source = "source";
+ String key = "foo:src/Foo.java";
+ FileUtils.write(ioFile, source, StandardCharsets.UTF_8);
+ when(file.key()).thenReturn(key);
+ when(file.status()).thenReturn(InputFile.Status.ADDED);
+
+ assertThat(sourceHashHolder.getHashedReference()).isNull();
+ Mockito.verifyNoMoreInteractions(lastSnapshots);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/TrackedIssueTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/TrackedIssueTest.java
new file mode 100644
index 00000000000..4228f912b94
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/issue/tracking/TrackedIssueTest.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+public class TrackedIssueTest {
+ @Test
+ public void round_trip() {
+ TrackedIssue issue = new TrackedIssue();
+ issue.setStartLine(15);
+
+ assertThat(issue.getLine()).isEqualTo(15);
+ assertThat(issue.startLine()).isEqualTo(15);
+ }
+
+ @Test
+ public void hashes() {
+ String[] hashArr = new String[] {
+ "hash1", "hash2", "hash3"
+ };
+
+ FileHashes hashes = FileHashes.create(hashArr);
+ TrackedIssue issue = new TrackedIssue(hashes);
+ issue.setStartLine(1);
+ assertThat(issue.getLineHash()).isEqualTo("hash1");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java
new file mode 100644
index 00000000000..619977ce156
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java
@@ -0,0 +1,497 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest;
+
+import org.sonar.api.rule.RuleKey;
+
+import org.sonar.batch.rule.LoadedActiveRule;
+import org.sonar.batch.repository.FileData;
+import org.sonar.api.utils.DateUtils;
+import com.google.common.collect.Table;
+import com.google.common.collect.HashBasedTable;
+import org.sonar.batch.repository.ProjectRepositories;
+import org.sonar.batch.rule.ActiveRulesLoader;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+import org.sonar.batch.repository.QualityProfileLoader;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+import javax.annotation.Nullable;
+
+import org.sonarqube.ws.Rules.ListResponse.Rule;
+import org.sonar.batch.bootstrapper.IssueListener;
+import org.sonar.api.server.rule.RulesDefinition.Repository;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.batch.rule.RulesLoader;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+import com.google.common.base.Function;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.sonar.api.CoreProperties;
+import org.sonar.api.SonarPlugin;
+import org.sonar.api.batch.debt.internal.DefaultDebtModel;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.batch.bootstrapper.Batch;
+import org.sonar.batch.bootstrapper.EnvironmentInformation;
+import org.sonar.batch.bootstrapper.LogOutput;
+import org.sonar.batch.issue.tracking.ServerLineHashesLoader;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.repository.GlobalRepositoriesLoader;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
+import org.sonar.batch.repository.ServerIssuesLoader;
+
+/**
+ * Main utility class for writing batch medium tests.
+ *
+ */
+public class BatchMediumTester {
+
+ public static final String MEDIUM_TEST_ENABLED = "sonar.mediumTest.enabled";
+ private Batch batch;
+ private static Path workingDir = null;
+ private static Path globalWorkingDir = null;
+
+ private static void createWorkingDirs() throws IOException {
+ destroyWorkingDirs();
+
+ workingDir = java.nio.file.Files.createTempDirectory("mediumtest-working-dir");
+ globalWorkingDir = java.nio.file.Files.createTempDirectory("mediumtest-global-working-dir");
+ }
+
+ private static void destroyWorkingDirs() throws IOException {
+ if (workingDir != null) {
+ FileUtils.deleteDirectory(workingDir.toFile());
+ workingDir = null;
+ }
+
+ if (globalWorkingDir != null) {
+ FileUtils.deleteDirectory(globalWorkingDir.toFile());
+ globalWorkingDir = null;
+ }
+
+ }
+
+ public static BatchMediumTesterBuilder builder() {
+ try {
+ createWorkingDirs();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ BatchMediumTesterBuilder builder = new BatchMediumTesterBuilder().registerCoreMetrics();
+ builder.bootstrapProperties.put(MEDIUM_TEST_ENABLED, "true");
+ builder.bootstrapProperties.put(ReportPublisher.KEEP_REPORT_PROP_KEY, "true");
+ builder.bootstrapProperties.put(CoreProperties.WORKING_DIRECTORY, workingDir.toString());
+ builder.bootstrapProperties.put("sonar.userHome", globalWorkingDir.toString());
+ return builder;
+ }
+
+ public static class BatchMediumTesterBuilder {
+ private final FakeGlobalRepositoriesLoader globalRefProvider = new FakeGlobalRepositoriesLoader();
+ private final FakeProjectRepositoriesLoader projectRefProvider = new FakeProjectRepositoriesLoader();
+ private final FakePluginInstaller pluginInstaller = new FakePluginInstaller();
+ private final FakeServerIssuesLoader serverIssues = new FakeServerIssuesLoader();
+ private final FakeServerLineHashesLoader serverLineHashes = new FakeServerLineHashesLoader();
+ private final Map<String, String> bootstrapProperties = new HashMap<>();
+ private final FakeRulesLoader rulesLoader = new FakeRulesLoader();
+ private final FakeQualityProfileLoader qualityProfiles = new FakeQualityProfileLoader();
+ private final FakeActiveRulesLoader activeRules = new FakeActiveRulesLoader();
+ private boolean associated = true;
+ private LogOutput logOutput = null;
+
+ public BatchMediumTester build() {
+ return new BatchMediumTester(this);
+ }
+
+ public BatchMediumTesterBuilder setAssociated(boolean associated) {
+ this.associated = associated;
+ return this;
+ }
+
+ public BatchMediumTesterBuilder setLogOutput(LogOutput logOutput) {
+ this.logOutput = logOutput;
+ return this;
+ }
+
+ public BatchMediumTesterBuilder registerPlugin(String pluginKey, File location) {
+ pluginInstaller.add(pluginKey, location);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder registerPlugin(String pluginKey, SonarPlugin instance) {
+ pluginInstaller.add(pluginKey, instance);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder registerCoreMetrics() {
+ for (Metric<?> m : CoreMetrics.getMetrics()) {
+ registerMetric(m);
+ }
+ return this;
+ }
+
+ public BatchMediumTesterBuilder registerMetric(Metric<?> metric) {
+ globalRefProvider.add(metric);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder addQProfile(String language, String name) {
+ qualityProfiles.add(language, name);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder addRule(Rule rule) {
+ rulesLoader.addRule(rule);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder addRule(String key, String repoKey, String internalKey, String name) {
+ Rule.Builder builder = Rule.newBuilder();
+ builder.setKey(key);
+ builder.setRepository(repoKey);
+ if (internalKey != null) {
+ builder.setInternalKey(internalKey);
+ }
+ builder.setName(name);
+
+ rulesLoader.addRule(builder.build());
+ return this;
+ }
+
+ public BatchMediumTesterBuilder addRules(RulesDefinition rulesDefinition) {
+ RulesDefinition.Context context = new RulesDefinition.Context();
+ rulesDefinition.define(context);
+ List<Repository> repositories = context.repositories();
+ for (Repository repo : repositories) {
+ for (RulesDefinition.Rule rule : repo.rules()) {
+ this.addRule(rule.key(), rule.repository().key(), rule.internalKey(), rule.name());
+ }
+ }
+ return this;
+ }
+
+ public BatchMediumTesterBuilder addDefaultQProfile(String language, String name) {
+ addQProfile(language, name);
+ globalRefProvider.globalSettings().put("sonar.profile." + language, name);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder setPreviousAnalysisDate(Date previousAnalysis) {
+ projectRefProvider.setLastAnalysisDate(previousAnalysis);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder bootstrapProperties(Map<String, String> props) {
+ bootstrapProperties.putAll(props);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder activateRule(LoadedActiveRule activeRule) {
+ activeRules.addActiveRule(activeRule);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder addActiveRule(String repositoryKey, String ruleKey, @Nullable String templateRuleKey, String name, @Nullable String severity,
+ @Nullable String internalKey, @Nullable String languag) {
+ LoadedActiveRule r = new LoadedActiveRule();
+
+ r.setInternalKey(internalKey);
+ r.setRuleKey(RuleKey.of(repositoryKey, ruleKey));
+ r.setName(name);
+ r.setTemplateRuleKey(templateRuleKey);
+ r.setLanguage(languag);
+ r.setSeverity(severity);
+
+ activeRules.addActiveRule(r);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder addFileData(String moduleKey, String path, FileData fileData) {
+ projectRefProvider.addFileData(moduleKey, path, fileData);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder setLastBuildDate(Date d) {
+ projectRefProvider.setLastAnalysisDate(d);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder mockServerIssue(ServerIssue issue) {
+ serverIssues.getServerIssues().add(issue);
+ return this;
+ }
+
+ public BatchMediumTesterBuilder mockLineHashes(String fileKey, String[] lineHashes) {
+ serverLineHashes.byKey.put(fileKey, lineHashes);
+ return this;
+ }
+
+ }
+
+ public void start() {
+ batch.start();
+ }
+
+ public void stop() {
+ batch.stop();
+ try {
+ destroyWorkingDirs();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void syncProject(String projectKey) {
+ batch.syncProject(projectKey);
+ }
+
+ private BatchMediumTester(BatchMediumTesterBuilder builder) {
+ Batch.Builder batchBuilder = Batch.builder()
+ .setEnableLoggingConfiguration(true)
+ .addComponents(
+ new EnvironmentInformation("mediumTest", "1.0"),
+ builder.pluginInstaller,
+ builder.globalRefProvider,
+ builder.qualityProfiles,
+ builder.rulesLoader,
+ builder.projectRefProvider,
+ builder.activeRules,
+ new DefaultDebtModel())
+ .setBootstrapProperties(builder.bootstrapProperties)
+ .setLogOutput(builder.logOutput);
+
+ if (builder.associated) {
+ batchBuilder.addComponents(
+ builder.serverIssues);
+ }
+ batch = batchBuilder.build();
+ }
+
+ public TaskBuilder newTask() {
+ return new TaskBuilder(this);
+ }
+
+ public TaskBuilder newScanTask(File sonarProps) {
+ Properties prop = new Properties();
+ try (Reader reader = new InputStreamReader(new FileInputStream(sonarProps), StandardCharsets.UTF_8)) {
+ prop.load(reader);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to read configuration file", e);
+ }
+ TaskBuilder builder = new TaskBuilder(this);
+ builder.property("sonar.projectBaseDir", sonarProps.getParentFile().getAbsolutePath());
+ for (Map.Entry<Object, Object> entry : prop.entrySet()) {
+ builder.property(entry.getKey().toString(), entry.getValue().toString());
+ }
+ return builder;
+ }
+
+ public static class TaskBuilder {
+ private final Map<String, String> taskProperties = new HashMap<>();
+ private BatchMediumTester tester;
+ private IssueListener issueListener = null;
+
+ public TaskBuilder(BatchMediumTester tester) {
+ this.tester = tester;
+ }
+
+ public TaskResult start() {
+ TaskResult result = new TaskResult();
+ Map<String, String> props = new HashMap<>();
+ props.putAll(taskProperties);
+ if (issueListener != null) {
+ tester.batch.executeTask(props, result, issueListener);
+ } else {
+ tester.batch.executeTask(props, result);
+ }
+ return result;
+ }
+
+ public TaskBuilder properties(Map<String, String> props) {
+ taskProperties.putAll(props);
+ return this;
+ }
+
+ public TaskBuilder property(String key, String value) {
+ taskProperties.put(key, value);
+ return this;
+ }
+
+ public TaskBuilder setIssueListener(IssueListener issueListener) {
+ this.issueListener = issueListener;
+ return this;
+ }
+ }
+
+ private static class FakeRulesLoader implements RulesLoader {
+ private List<org.sonarqube.ws.Rules.ListResponse.Rule> rules = new LinkedList<>();
+
+ public FakeRulesLoader addRule(Rule rule) {
+ rules.add(rule);
+ return this;
+ }
+
+ @Override
+ public List<Rule> load(@Nullable MutableBoolean fromCache) {
+ return rules;
+ }
+ }
+
+ private static class FakeActiveRulesLoader implements ActiveRulesLoader {
+ private List<LoadedActiveRule> activeRules = new LinkedList<>();
+
+ public void addActiveRule(LoadedActiveRule activeRule) {
+ this.activeRules.add(activeRule);
+ }
+
+ @Override
+ public List<LoadedActiveRule> load(String qualityProfileKey, MutableBoolean fromCache) {
+ return activeRules;
+ }
+ }
+
+ private static class FakeGlobalRepositoriesLoader implements GlobalRepositoriesLoader {
+
+ private int metricId = 1;
+
+ private GlobalRepositories ref = new GlobalRepositories();
+
+ @Override
+ public GlobalRepositories load(@Nullable MutableBoolean fromCache) {
+ return ref;
+ }
+
+ public Map<String, String> globalSettings() {
+ return ref.globalSettings();
+ }
+
+ public FakeGlobalRepositoriesLoader add(Metric<?> metric) {
+ Boolean optimizedBestValue = metric.isOptimizedBestValue();
+ ref.metrics().add(new org.sonar.scanner.protocol.input.Metric(metricId,
+ metric.key(),
+ metric.getType().name(),
+ metric.getDescription(),
+ metric.getDirection(),
+ metric.getName(),
+ metric.getQualitative(),
+ metric.getUserManaged(),
+ metric.getWorstValue(),
+ metric.getBestValue(),
+ optimizedBestValue != null ? optimizedBestValue : false));
+ metricId++;
+ return this;
+ }
+
+ }
+
+ private static class FakeProjectRepositoriesLoader implements ProjectRepositoriesLoader {
+
+ private Table<String, String, FileData> fileDataTable = HashBasedTable.create();
+ private Date lastAnalysisDate;
+
+ @Override
+ public ProjectRepositories load(String projectKey, boolean isIssuesMode, @Nullable MutableBoolean fromCache) {
+ Table<String, String, String> settings = HashBasedTable.create();
+ return new ProjectRepositories(settings, fileDataTable, lastAnalysisDate);
+ }
+
+ public FakeProjectRepositoriesLoader addFileData(String moduleKey, String path, FileData fileData) {
+ fileDataTable.put(moduleKey, path, fileData);
+ return this;
+ }
+
+ public FakeProjectRepositoriesLoader setLastAnalysisDate(Date d) {
+ lastAnalysisDate = d;
+ return this;
+ }
+
+ }
+
+ private static class FakeQualityProfileLoader implements QualityProfileLoader {
+
+ private List<QualityProfile> qualityProfiles = new LinkedList<>();
+
+ public void add(String language, String name) {
+ qualityProfiles.add(QualityProfile.newBuilder()
+ .setLanguage(language)
+ .setKey(name)
+ .setName(name)
+ .setRulesUpdatedAt(DateUtils.formatDateTime(new Date(1234567891212L)))
+ .build());
+ }
+
+ @Override
+ public List<QualityProfile> load(String projectKey, String profileName, MutableBoolean fromCache) {
+ return qualityProfiles;
+ }
+
+ @Override
+ public List<QualityProfile> loadDefault(String profileName, MutableBoolean fromCache) {
+ return qualityProfiles;
+ }
+ }
+
+ private static class FakeServerIssuesLoader implements ServerIssuesLoader {
+
+ private List<ServerIssue> serverIssues = new ArrayList<>();
+
+ public List<ServerIssue> getServerIssues() {
+ return serverIssues;
+ }
+
+ @Override
+ public boolean load(String componentKey, Function<ServerIssue, Void> consumer) {
+ for (ServerIssue serverIssue : serverIssues) {
+ consumer.apply(serverIssue);
+ }
+ return true;
+ }
+ }
+
+ private static class FakeServerLineHashesLoader implements ServerLineHashesLoader {
+ private Map<String, String[]> byKey = new HashMap<>();
+
+ @Override
+ public String[] getLineHashes(String fileKey, @Nullable MutableBoolean fromCache) {
+ if (byKey.containsKey(fileKey)) {
+ return byKey.get(fileKey);
+ } else {
+ throw new IllegalStateException("You forgot to mock line hashes for " + fileKey);
+ }
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/LogOutputRecorder.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/LogOutputRecorder.java
new file mode 100644
index 00000000000..edae5e47fb1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/LogOutputRecorder.java
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.google.common.collect.Multimap;
+import com.google.common.collect.HashMultimap;
+import org.sonar.batch.bootstrapper.LogOutput;
+
+public class LogOutputRecorder implements LogOutput {
+ private Multimap<String, String> recordedByLevel = HashMultimap.create();
+ private List<String> recorded = new LinkedList<>();
+ private StringBuffer asString = new StringBuffer();
+
+ @Override
+ public void log(String formattedMessage, Level level) {
+ recordedByLevel.put(level.toString(), formattedMessage);
+ recorded.add(formattedMessage);
+ asString.append(formattedMessage).append("\n");
+ }
+
+ public Collection<String> getAll() {
+ return recorded;
+ }
+
+ public Collection<String> get(String level) {
+ return recordedByLevel.get(level);
+ }
+
+ public String getAsString() {
+ return asString.toString();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cache/CacheSyncTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cache/CacheSyncTest.java
new file mode 100644
index 00000000000..4ebf5eb1fa6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cache/CacheSyncTest.java
@@ -0,0 +1,155 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.cache;
+
+import org.junit.rules.TemporaryFolder;
+
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.batch.mediumtest.BatchMediumTester.TaskBuilder;
+import org.sonar.batch.mediumtest.LogOutputRecorder;
+import org.sonar.batch.repository.FileData;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.CoreProperties;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+public class CacheSyncTest {
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private BatchMediumTester tester;
+
+ @After
+ public void stop() {
+ if (tester != null) {
+ tester.stop();
+ tester = null;
+ }
+ }
+
+ @Test
+ public void testExecuteTask() {
+ LogOutputRecorder logOutput = new LogOutputRecorder();
+
+ tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES,
+ "sonar.verbose", "true"))
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addQProfile("lang", "name")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "my/internal/key", "xoo")
+ .setPreviousAnalysisDate(new Date())
+ .addFileData("test-project", "file1", new FileData("hash", "123456789"))
+ .setLogOutput(logOutput)
+ .build();
+
+ tester.start();
+ executeTask(tester.newTask());
+ assertThat(logOutput.getAsString()).contains("Cache for project [key] not found, synchronizing");
+ }
+
+ @Test
+ public void testSyncFirstTime() {
+ LogOutputRecorder logOutput = new LogOutputRecorder();
+
+ tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES,
+ "sonar.verbose", "true"))
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addQProfile("lang", "name")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "my/internal/key", "xoo")
+ .setPreviousAnalysisDate(new Date())
+ .addFileData("test-project", "file1", new FileData("hash", "123456789"))
+ .setLogOutput(logOutput)
+ .build();
+
+ tester.start();
+ tester.syncProject("test-project");
+ assertThat(logOutput.getAsString()).contains("Cache for project [test-project] not found");
+ }
+
+ @Test
+ public void testSyncTwice() {
+ LogOutputRecorder logOutput = new LogOutputRecorder();
+
+ tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES,
+ "sonar.verbose", "true"))
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addQProfile("lang", "name")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "my/internal/key", "xoo")
+ .setPreviousAnalysisDate(new Date())
+ .addFileData("test-project", "file1", new FileData("hash", "123456789"))
+ .setLogOutput(logOutput)
+ .build();
+
+ tester.start();
+ tester.syncProject("test-project");
+ tester.syncProject("test-project");
+ assertThat(logOutput.getAsString()).contains("-- Found project [test-project]");
+ assertThat(logOutput.getAsString()).contains("not found, synchronizing data");
+ assertThat(logOutput.getAsString()).contains("], synchronizing data (forced)..");
+ }
+
+ @Test
+ public void testNonAssociated() {
+ LogOutputRecorder logOutput = new LogOutputRecorder();
+
+ tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES))
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addQProfile("lang", "name")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "my/internal/key", "xoo")
+ .setPreviousAnalysisDate(new Date())
+ .addFileData("test-project", "file1", new FileData("hash", "123456789"))
+ .setLogOutput(logOutput)
+ .build();
+
+ tester.start();
+ tester.syncProject(null);
+
+ assertThat(logOutput.getAsString()).contains("Cache not found, synchronizing data");
+ }
+
+ private TaskResult executeTask(TaskBuilder builder) {
+ builder.property("sonar.projectKey", "key");
+ builder.property("sonar.projectVersion", "1.0");
+ builder.property("sonar.projectName", "key");
+ builder.property("sonar.projectBaseDir", temp.getRoot().getAbsolutePath());
+ builder.property("sonar.sources", temp.getRoot().getAbsolutePath());
+ return builder.start();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/coverage/CoverageMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/coverage/CoverageMediumTest.java
new file mode 100644
index 00000000000..e3dac6ed869
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/coverage/CoverageMediumTest.java
@@ -0,0 +1,180 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.coverage;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class CoverageMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void unitTests() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xooUtCoverageFile = new File(srcDir, "sample.xoo.coverage");
+ FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}");
+ FileUtils.write(xooUtCoverageFile, "2:2:2:1\n3:1");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ InputFile file = result.inputFile("src/sample.xoo");
+ assertThat(result.coverageFor(file, 2).getUtHits()).isTrue();
+ assertThat(result.coverageFor(file, 2).getItHits()).isFalse();
+ assertThat(result.coverageFor(file, 2).getConditions()).isEqualTo(2);
+ assertThat(result.coverageFor(file, 2).getUtCoveredConditions()).isEqualTo(1);
+ assertThat(result.coverageFor(file, 2).getItCoveredConditions()).isEqualTo(0);
+ assertThat(result.coverageFor(file, 2).getOverallCoveredConditions()).isEqualTo(0);
+
+ Map<String, List<org.sonar.scanner.protocol.output.ScannerReport.Measure>> allMeasures = result.allMeasures();
+ assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey", "intValue")
+ .contains(tuple(CoreMetrics.LINES_TO_COVER_KEY, 2),
+ tuple(CoreMetrics.UNCOVERED_LINES_KEY, 0),
+ tuple(CoreMetrics.CONDITIONS_TO_COVER_KEY, 2),
+ tuple(CoreMetrics.UNCOVERED_CONDITIONS_KEY, 1));
+ }
+
+ @Test
+ public void exclusions() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xooUtCoverageFile = new File(srcDir, "sample.xoo.coverage");
+ FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}");
+ FileUtils.write(xooUtCoverageFile, "2:2:2:1\n3:1");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.coverage.exclusions", "**/sample.xoo")
+ .build())
+ .start();
+
+ InputFile file = result.inputFile("src/sample.xoo");
+ assertThat(result.coverageFor(file, 2)).isNull();
+
+ Map<String, List<org.sonar.scanner.protocol.output.ScannerReport.Measure>> allMeasures = result.allMeasures();
+ assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey")
+ .doesNotContain(CoreMetrics.LINES_TO_COVER_KEY, CoreMetrics.UNCOVERED_LINES_KEY, CoreMetrics.CONDITIONS_TO_COVER_KEY,
+ CoreMetrics.COVERED_CONDITIONS_BY_LINE_KEY);
+ }
+
+ @Test
+ public void fallbackOnExecutableLines() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File measuresFile = new File(srcDir, "sample.xoo.measures");
+ FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}");
+ FileUtils.write(measuresFile, "executable_lines_data:2=1;3=1;4=0");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ InputFile file = result.inputFile("src/sample.xoo");
+ assertThat(result.coverageFor(file, 1)).isNull();
+
+ assertThat(result.coverageFor(file, 2).getUtHits()).isFalse();
+ assertThat(result.coverageFor(file, 2).getItHits()).isFalse();
+ assertThat(result.coverageFor(file, 2).getConditions()).isEqualTo(0);
+ assertThat(result.coverageFor(file, 2).getUtCoveredConditions()).isEqualTo(0);
+ assertThat(result.coverageFor(file, 2).getItCoveredConditions()).isEqualTo(0);
+ assertThat(result.coverageFor(file, 2).getOverallCoveredConditions()).isEqualTo(0);
+
+ assertThat(result.coverageFor(file, 3).getUtHits()).isFalse();
+ assertThat(result.coverageFor(file, 4)).isNull();
+
+ Map<String, List<org.sonar.scanner.protocol.output.ScannerReport.Measure>> allMeasures = result.allMeasures();
+ assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey", "intValue")
+ .contains(tuple(CoreMetrics.LINES_TO_COVER_KEY, 2),
+ tuple(CoreMetrics.UNCOVERED_LINES_KEY, 2));
+
+ assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey").doesNotContain(CoreMetrics.CONDITIONS_TO_COVER_KEY, CoreMetrics.UNCOVERED_CONDITIONS_KEY);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java
new file mode 100644
index 00000000000..662ed7dbcfd
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java
@@ -0,0 +1,336 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.cpd;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.assertj.core.groups.Tuple;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReport.Measure;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.lang.CpdTokenizerSensor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(Parameterized.class)
+public class CpdMediumTest {
+
+ @Parameters(name = "new api: {0}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+ {true}, {false}
+ });
+ }
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ private File baseDir;
+
+ private ImmutableMap.Builder<String, String> builder;
+
+ private boolean useNewSensorApi;
+
+ public CpdMediumTest(boolean useNewSensorApi) {
+ this.useNewSensorApi = useNewSensorApi;
+ }
+
+ @Before
+ public void prepare() throws IOException {
+ tester.start();
+
+ baseDir = temp.getRoot();
+
+ builder = ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project");
+ if (useNewSensorApi) {
+ builder.put(CpdTokenizerSensor.ENABLE_PROP, "true");
+ }
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testCrossModuleDuplications() throws IOException {
+ builder.put("sonar.modules", "module1,module2")
+ .put("sonar.cpd.xoo.minimumTokens", "10")
+ .put("sonar.verbose", "true");
+
+ // module 1
+ builder.put("module1.sonar.projectKey", "module1");
+ builder.put("module1.sonar.projectName", "Module 1");
+ builder.put("module1.sonar.sources", ".");
+
+ // module2
+ builder.put("module2.sonar.projectKey", "module2");
+ builder.put("module2.sonar.projectName", "Module 2");
+ builder.put("module2.sonar.sources", ".");
+
+ File module1Dir = new File(baseDir, "module1");
+ File module2Dir = new File(baseDir, "module2");
+
+ module1Dir.mkdir();
+ module2Dir.mkdir();
+
+ String duplicatedStuff = "Sample xoo\ncontent\n"
+ + "foo\nbar\ntoto\ntiti\n"
+ + "foo\nbar\ntoto\ntiti\n"
+ + "bar\ntoto\ntiti\n"
+ + "foo\nbar\ntoto\ntiti";
+
+ // create duplicated file in both modules
+ File xooFile1 = new File(module1Dir, "sample1.xoo");
+ FileUtils.write(xooFile1, duplicatedStuff);
+
+ File xooFile2 = new File(module2Dir, "sample2.xoo");
+ FileUtils.write(xooFile2, duplicatedStuff);
+
+ TaskResult result = tester.newTask().properties(builder.build()).start();
+
+ assertThat(result.inputFiles()).hasSize(2);
+
+ InputFile inputFile1 = result.inputFile("sample1.xoo");
+ InputFile inputFile2 = result.inputFile("sample2.xoo");
+
+ // One clone group on each file
+ List<org.sonar.scanner.protocol.output.ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1);
+ assertThat(duplicationGroupsFile1).hasSize(1);
+
+ org.sonar.scanner.protocol.output.ScannerReport.Duplication cloneGroupFile1 = duplicationGroupsFile1.get(0);
+ assertThat(cloneGroupFile1.getOriginPosition().getStartLine()).isEqualTo(1);
+ assertThat(cloneGroupFile1.getOriginPosition().getEndLine()).isEqualTo(17);
+ assertThat(cloneGroupFile1.getDuplicateList()).hasSize(1);
+ assertThat(cloneGroupFile1.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(((DefaultInputFile) inputFile2).key()).getRef());
+
+ List<org.sonar.scanner.protocol.output.ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2);
+ assertThat(duplicationGroupsFile2).hasSize(1);
+
+ org.sonar.scanner.protocol.output.ScannerReport.Duplication cloneGroupFile2 = duplicationGroupsFile2.get(0);
+ assertThat(cloneGroupFile2.getOriginPosition().getStartLine()).isEqualTo(1);
+ assertThat(cloneGroupFile2.getOriginPosition().getEndLine()).isEqualTo(17);
+ assertThat(cloneGroupFile2.getDuplicateList()).hasSize(1);
+ assertThat(cloneGroupFile2.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(((DefaultInputFile) inputFile1).key()).getRef());
+
+ assertThat(result.duplicationBlocksFor(inputFile1)).isEmpty();
+ }
+
+ @Test
+ public void testCrossFileDuplications() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ String duplicatedStuff = "Sample xoo\ncontent\n"
+ + "foo\nbar\ntoto\ntiti\n"
+ + "foo\nbar\ntoto\ntiti\n"
+ + "bar\ntoto\ntiti\n"
+ + "foo\nbar\ntoto\ntiti";
+
+ File xooFile1 = new File(srcDir, "sample1.xoo");
+ FileUtils.write(xooFile1, duplicatedStuff);
+
+ File xooFile2 = new File(srcDir, "sample2.xoo");
+ FileUtils.write(xooFile2, duplicatedStuff);
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.cpd.xoo.minimumTokens", "10")
+ .put("sonar.verbose", "true")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(2);
+
+ InputFile inputFile1 = result.inputFile("src/sample1.xoo");
+ InputFile inputFile2 = result.inputFile("src/sample2.xoo");
+
+ // One clone group on each file
+ List<org.sonar.scanner.protocol.output.ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1);
+ assertThat(duplicationGroupsFile1).hasSize(1);
+
+ org.sonar.scanner.protocol.output.ScannerReport.Duplication cloneGroupFile1 = duplicationGroupsFile1.get(0);
+ assertThat(cloneGroupFile1.getOriginPosition().getStartLine()).isEqualTo(1);
+ assertThat(cloneGroupFile1.getOriginPosition().getEndLine()).isEqualTo(17);
+ assertThat(cloneGroupFile1.getDuplicateList()).hasSize(1);
+ assertThat(cloneGroupFile1.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(((DefaultInputFile) inputFile2).key()).getRef());
+
+ List<org.sonar.scanner.protocol.output.ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2);
+ assertThat(duplicationGroupsFile2).hasSize(1);
+
+ org.sonar.scanner.protocol.output.ScannerReport.Duplication cloneGroupFile2 = duplicationGroupsFile2.get(0);
+ assertThat(cloneGroupFile2.getOriginPosition().getStartLine()).isEqualTo(1);
+ assertThat(cloneGroupFile2.getOriginPosition().getEndLine()).isEqualTo(17);
+ assertThat(cloneGroupFile2.getDuplicateList()).hasSize(1);
+ assertThat(cloneGroupFile2.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(((DefaultInputFile) inputFile1).key()).getRef());
+
+ assertThat(result.duplicationBlocksFor(inputFile1)).isEmpty();
+ }
+
+ @Test
+ public void enableCrossProjectDuplication() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ String duplicatedStuff = "Sample xoo\ncontent\nfoo\nbar\ntoto\ntiti\nfoo";
+
+ File xooFile1 = new File(srcDir, "sample1.xoo");
+ FileUtils.write(xooFile1, duplicatedStuff);
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.cpd.xoo.minimumTokens", "1")
+ .put("sonar.cpd.xoo.minimumLines", "5")
+ .put("sonar.verbose", "true")
+ .put("sonar.cpd.cross_project", "true")
+ .build())
+ .start();
+
+ InputFile inputFile1 = result.inputFile("src/sample1.xoo");
+
+ List<ScannerReport.CpdTextBlock> duplicationBlocks = result.duplicationBlocksFor(inputFile1);
+ assertThat(duplicationBlocks).hasSize(3);
+ assertThat(duplicationBlocks.get(0).getStartLine()).isEqualTo(1);
+ assertThat(duplicationBlocks.get(0).getEndLine()).isEqualTo(5);
+ assertThat(duplicationBlocks.get(0).getStartTokenIndex()).isEqualTo(1);
+ assertThat(duplicationBlocks.get(0).getEndTokenIndex()).isEqualTo(6);
+ assertThat(duplicationBlocks.get(0).getHash()).isNotEmpty();
+
+ assertThat(duplicationBlocks.get(1).getStartLine()).isEqualTo(2);
+ assertThat(duplicationBlocks.get(1).getEndLine()).isEqualTo(6);
+ assertThat(duplicationBlocks.get(1).getStartTokenIndex()).isEqualTo(3);
+ assertThat(duplicationBlocks.get(1).getEndTokenIndex()).isEqualTo(7);
+ assertThat(duplicationBlocks.get(0).getHash()).isNotEmpty();
+
+ assertThat(duplicationBlocks.get(2).getStartLine()).isEqualTo(3);
+ assertThat(duplicationBlocks.get(2).getEndLine()).isEqualTo(7);
+ assertThat(duplicationBlocks.get(2).getStartTokenIndex()).isEqualTo(4);
+ assertThat(duplicationBlocks.get(2).getEndTokenIndex()).isEqualTo(8);
+ assertThat(duplicationBlocks.get(0).getHash()).isNotEmpty();
+ }
+
+ // SONAR-6000
+ @Test
+ public void truncateDuplication() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ String duplicatedStuff = "Sample xoo\n";
+
+ int blockCount = 10000;
+ File xooFile1 = new File(srcDir, "sample.xoo");
+ for (int i = 0; i < blockCount; i++) {
+ FileUtils.write(xooFile1, duplicatedStuff, true);
+ FileUtils.write(xooFile1, "" + i + "\n", true);
+ }
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.cpd.xoo.minimumTokens", "1")
+ .put("sonar.cpd.xoo.minimumLines", "1")
+ .build())
+ .start();
+
+ Map<String, List<Measure>> allMeasures = result.allMeasures();
+
+ assertThat(allMeasures.get("com.foo.project")).extracting("metricKey", "intValue", "doubleValue", "stringValue").containsOnly(
+ Tuple.tuple(CoreMetrics.QUALITY_PROFILES_KEY, 0, 0.0,
+ "[{\"key\":\"Sonar Way\",\"language\":\"xoo\",\"name\":\"Sonar Way\",\"rulesUpdatedAt\":\"2009-02-13T23:31:31+0000\"}]"));
+
+ assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey", "intValue").containsOnly(
+ Tuple.tuple(CoreMetrics.LINES_KEY, blockCount * 2 + 1));
+
+ List<org.sonar.scanner.protocol.output.ScannerReport.Duplication> duplicationGroups = result.duplicationsFor(result.inputFile("src/sample.xoo"));
+ assertThat(duplicationGroups).hasSize(1);
+
+ org.sonar.scanner.protocol.output.ScannerReport.Duplication cloneGroup = duplicationGroups.get(0);
+ assertThat(cloneGroup.getDuplicateList()).hasSize(100);
+ }
+
+ @Test
+ public void testIntraFileDuplications() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ String content = "Sample xoo\ncontent\nfoo\nbar\nSample xoo\ncontent\n";
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, content);
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.cpd.xoo.minimumTokens", "2")
+ .put("sonar.cpd.xoo.minimumLines", "2")
+ .put("sonar.verbose", "true")
+ .build())
+ .start();
+
+ InputFile inputFile = result.inputFile("src/sample.xoo");
+ // One clone group
+ List<org.sonar.scanner.protocol.output.ScannerReport.Duplication> duplicationGroups = result.duplicationsFor(inputFile);
+ assertThat(duplicationGroups).hasSize(1);
+
+ org.sonar.scanner.protocol.output.ScannerReport.Duplication cloneGroup = duplicationGroups.get(0);
+ assertThat(cloneGroup.getOriginPosition().getStartLine()).isEqualTo(1);
+ assertThat(cloneGroup.getOriginPosition().getEndLine()).isEqualTo(2);
+ assertThat(cloneGroup.getDuplicateList()).hasSize(1);
+ assertThat(cloneGroup.getDuplicate(0).getRange().getStartLine()).isEqualTo(5);
+ assertThat(cloneGroup.getDuplicate(0).getRange().getEndLine()).isEqualTo(6);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/deprecated/DeprecatedApiMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/deprecated/DeprecatedApiMediumTest.java
new file mode 100644
index 00000000000..fa1ff38833d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/deprecated/DeprecatedApiMediumTest.java
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.deprecated;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+
+public class DeprecatedApiMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addActiveRule("xoo", "DeprecatedResourceApi", null, "One issue per line", "MAJOR", null, "xoo")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testIssueDetails() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFileInRootDir = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFileInRootDir, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
+
+ File xooFileInAnotherDir = new File(srcDir, "package/sample.xoo");
+ FileUtils.write(xooFileInAnotherDir, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ assertThat(result.issuesFor(result.inputFile("src/sample.xoo"))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0),
+ tuple("Issue created using deprecated API", 1));
+ assertThat(result.issuesFor(result.inputFile("src/package/sample.xoo"))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0),
+ tuple("Issue created using deprecated API", 1));
+ assertThat(result.issuesFor(result.inputDir("src"))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0));
+ assertThat(result.issuesFor(result.inputDir("src/package"))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0));
+
+ }
+
+ @Test
+ public void createIssueOnRootDir() throws IOException {
+
+ File baseDir = temp.getRoot();
+
+ File xooFileInRootDir = new File(baseDir, "sample.xoo");
+ FileUtils.write(xooFileInRootDir, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
+
+ File xooFileInAnotherDir = new File(baseDir, "package/sample.xoo");
+ FileUtils.write(xooFileInAnotherDir, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", ".")
+ .build())
+ .start();
+
+ assertThat(result.issuesFor(result.inputFile("sample.xoo"))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0),
+ tuple("Issue created using deprecated API", 1));
+ assertThat(result.issuesFor(result.inputFile("package/sample.xoo"))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0),
+ tuple("Issue created using deprecated API", 1));
+ assertThat(result.issuesFor(result.inputDir(""))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0));
+ assertThat(result.issuesFor(result.inputDir("package"))).extracting("msg", "line").containsOnly(
+ tuple("Issue created using deprecated API", 0));
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/FileSystemMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/FileSystemMediumTest.java
new file mode 100644
index 00000000000..dcb32c1030e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/FileSystemMediumTest.java
@@ -0,0 +1,296 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.fs;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.System2;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FileSystemMediumTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ private File baseDir;
+
+ private ImmutableMap.Builder<String, String> builder;
+
+ @Before
+ public void prepare() throws IOException {
+ tester.start();
+
+ baseDir = temp.getRoot();
+
+ builder = ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project");
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void scanProjectWithSourceDir() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(1);
+ assertThat(result.inputDirs()).hasSize(1);
+ assertThat(result.inputFile("src/sample.xoo").type()).isEqualTo(InputFile.Type.MAIN);
+ assertThat(result.inputFile("src/sample.xoo").relativePath()).isEqualTo("src/sample.xoo");
+ assertThat(result.inputDir("src").relativePath()).isEqualTo("src");
+ }
+
+ @Test
+ public void scanBigProject() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ int nbFiles = 100;
+ int ruleCount = 100000;
+ for (int nb = 1; nb <= nbFiles; nb++) {
+ File xooFile = new File(srcDir, "sample" + nb + ".xoo");
+ FileUtils.write(xooFile, StringUtils.repeat(StringUtils.repeat("a", 100) + "\n", ruleCount / 1000));
+ }
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(100);
+ assertThat(result.inputDirs()).hasSize(1);
+ }
+
+ @Test
+ public void scanProjectWithTestDir() throws IOException {
+ File test = new File(baseDir, "test");
+ test.mkdir();
+
+ File xooFile = new File(test, "sampleTest.xoo");
+ FileUtils.write(xooFile, "Sample test xoo\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "")
+ .put("sonar.tests", "test")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(1);
+ assertThat(result.inputFile("test/sampleTest.xoo").type()).isEqualTo(InputFile.Type.TEST);
+ }
+
+ /**
+ * SONAR-5419
+ */
+ @Test
+ public void scanProjectWithMixedSourcesAndTests() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ File xooFile2 = new File(baseDir, "another.xoo");
+ FileUtils.write(xooFile2, "Sample xoo 2\ncontent");
+
+ File testDir = new File(baseDir, "test");
+ testDir.mkdir();
+
+ File xooTestFile = new File(baseDir, "sampleTest2.xoo");
+ FileUtils.write(xooTestFile, "Sample test xoo\ncontent");
+
+ File xooTestFile2 = new File(testDir, "sampleTest.xoo");
+ FileUtils.write(xooTestFile2, "Sample test xoo 2\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src,another.xoo")
+ .put("sonar.tests", "test,sampleTest2.xoo")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(4);
+ }
+
+ @Test
+ public void fileInclusionsExclusions() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ File xooFile2 = new File(baseDir, "another.xoo");
+ FileUtils.write(xooFile2, "Sample xoo 2\ncontent");
+
+ File testDir = new File(baseDir, "test");
+ testDir.mkdir();
+
+ File xooTestFile = new File(baseDir, "sampleTest2.xoo");
+ FileUtils.write(xooTestFile, "Sample test xoo\ncontent");
+
+ File xooTestFile2 = new File(testDir, "sampleTest.xoo");
+ FileUtils.write(xooTestFile2, "Sample test xoo 2\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src,another.xoo")
+ .put("sonar.tests", "test,sampleTest2.xoo")
+ .put("sonar.inclusions", "src/**")
+ .put("sonar.exclusions", "**/another.*")
+ .put("sonar.test.inclusions", "**/sampleTest*.*")
+ .put("sonar.test.exclusions", "**/sampleTest2.xoo")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(2);
+ }
+
+ @Test
+ public void failForDuplicateInputFile() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("can't be indexed twice. Please check that inclusion/exclusion patterns produce disjoint sets for main and test files");
+ tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src,src/sample.xoo")
+ .build())
+ .start();
+
+ }
+
+ // SONAR-5330
+ @Test
+ public void scanProjectWithSourceSymlink() {
+ if (!System2.INSTANCE.isOsWindows()) {
+ File projectDir = new File("src/test/resources/mediumtest/xoo/sample-with-symlink");
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(3);
+ // check that symlink was not resolved to target
+ assertThat(result.inputFiles()).extractingResultOf("path").toString().startsWith(projectDir.toString());
+ }
+ }
+
+ // SONAR-6719
+ @Test
+ public void scanProjectWithWrongCase() {
+ if (System2.INSTANCE.isOsWindows()) {
+ File projectDir = new File("src/test/resources/mediumtest/xoo/sample");
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.sources", "XOURCES")
+ .property("sonar.tests", "TESTX")
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(3);
+ assertThat(result.inputFiles()).extractingResultOf("relativePath").containsOnly(
+ "xources/hello/HelloJava.xoo",
+ "xources/hello/helloscala.xoo",
+ "testx/ClassOneTest.xoo");
+ }
+ }
+
+ @Test
+ public void indexAnyFile() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ File otherFile = new File(srcDir, "sample.other");
+ FileUtils.write(otherFile, "Sample other\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.import_unknown_files", "true")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(2);
+ assertThat(result.inputFile("src/sample.other").type()).isEqualTo(InputFile.Type.MAIN);
+ assertThat(result.inputFile("src/sample.other").relativePath()).isEqualTo("src/sample.other");
+ assertThat(result.inputFile("src/sample.other").language()).isNull();
+ }
+
+ @Test
+ public void scanMultiModuleProject() {
+ File projectDir = new File("src/test/resources/mediumtest/xoo/multi-modules-sample");
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(4);
+ assertThat(result.inputDirs()).hasSize(4);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/NoLanguagesPluginsMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/NoLanguagesPluginsMediumTest.java
new file mode 100644
index 00000000000..90502c54ae8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/NoLanguagesPluginsMediumTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.fs;
+
+import org.junit.rules.ExpectedException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.FileFilterUtils;
+import org.sonar.batch.mediumtest.issuesmode.IssueModeAndReportsMediumTest;
+
+import java.io.File;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+
+public class NoLanguagesPluginsMediumTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .setPreviousAnalysisDate(null)
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testNoLanguagePluginsInstalled() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("No language plugins are installed");
+
+ tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .start();
+ }
+
+ private File copyProject(String path) throws Exception {
+ File projectDir = temp.newFolder();
+ File originalProjectDir = new File(IssueModeAndReportsMediumTest.class.getResource(path).toURI());
+ FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar")));
+ return projectDir;
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/ProjectBuilderMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/ProjectBuilderMediumTest.java
new file mode 100644
index 00000000000..21526ddcfd5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/fs/ProjectBuilderMediumTest.java
@@ -0,0 +1,168 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.fs;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.output.ScannerReport.Issue;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectBuilderMediumTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .setPreviousAnalysisDate(new Date())
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "OneIssuePerLine.internal", "xoo")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testProjectBuilder() throws IOException {
+ File baseDir = prepareProject();
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", ".")
+ .put("sonar.xoo.enableProjectBuilder", "true")
+ .build())
+ .start();
+ List<Issue> issues = result.issuesFor(result.inputFile("src/sample.xoo"));
+ assertThat(issues).hasSize(10);
+
+ boolean foundIssueAtLine1 = false;
+ for (Issue issue : issues) {
+ if (issue.getLine() == 1) {
+ foundIssueAtLine1 = true;
+ assertThat(issue.getMsg()).isEqualTo("This issue is generated on each line");
+ assertThat(issue.hasGap()).isFalse();
+ }
+ }
+ assertThat(foundIssueAtLine1).isTrue();
+
+ }
+
+ @Test
+ // SONAR-6976
+ public void testProjectBuilderWithNewLine() throws IOException {
+ File baseDir = prepareProject();
+
+ exception.expect(MessageException.class);
+ exception.expectMessage("is not a valid branch name");
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.branch", "branch\n")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", ".")
+ .put("sonar.xoo.enableProjectBuilder", "true")
+ .build())
+ .start();
+ }
+
+ @Test
+ public void testProjectBuilderWithBranch() throws IOException {
+ File baseDir = prepareProject();
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.branch", "my-branch")
+ .put("sonar.sources", ".")
+ .put("sonar.xoo.enableProjectBuilder", "true")
+ .build())
+ .start();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("src/sample.xoo"));
+ assertThat(issues).hasSize(10);
+
+ boolean foundIssueAtLine1 = false;
+ for (Issue issue : issues) {
+ if (issue.getLine() == 1) {
+ foundIssueAtLine1 = true;
+ assertThat(issue.getMsg()).isEqualTo("This issue is generated on each line");
+ assertThat(issue.hasGap()).isFalse();
+ }
+ }
+ assertThat(foundIssueAtLine1).isTrue();
+ }
+
+ private File prepareProject() throws IOException {
+ File baseDir = temp.getRoot();
+ File module1Dir = new File(baseDir, "module1");
+ module1Dir.mkdir();
+
+ File srcDir = new File(module1Dir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
+
+ return baseDir;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/highlighting/HighlightingMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/highlighting/HighlightingMediumTest.java
new file mode 100644
index 00000000000..3c83ed61fcf
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/highlighting/HighlightingMediumTest.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.highlighting;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HighlightingMediumTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void computeSyntaxHighlightingOnTempProject() throws IOException {
+
+ File baseDir = temp.newFolder();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xoohighlightingFile = new File(srcDir, "sample.xoo.highlighting");
+ FileUtils.write(xooFile, "Sample xoo\ncontent plop");
+ FileUtils.write(xoohighlightingFile, "0:10:s\n11:18:k");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ InputFile file = result.inputFile("src/sample.xoo");
+ assertThat(result.highlightingTypeFor(file, 1, 0)).containsExactly(TypeOfText.STRING);
+ assertThat(result.highlightingTypeFor(file, 1, 9)).containsExactly(TypeOfText.STRING);
+ assertThat(result.highlightingTypeFor(file, 2, 0)).containsExactly(TypeOfText.KEYWORD);
+ assertThat(result.highlightingTypeFor(file, 2, 8)).isEmpty();
+ }
+
+ @Test
+ public void computeInvalidOffsets() throws IOException {
+
+ File baseDir = temp.newFolder();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xoohighlightingFile = new File(srcDir, "sample.xoo.highlighting");
+ FileUtils.write(xooFile, "Sample xoo\ncontent plop");
+ FileUtils.write(xoohighlightingFile, "0:10:s\n18:18:k");
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Error processing line 2");
+ exception.expectCause(new TypeSafeMatcher<IllegalArgumentException>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Invalid cause");
+ }
+
+ @Override
+ protected boolean matchesSafely(IllegalArgumentException e) {
+ return e.getMessage().contains("Unable to highlight file");
+ }
+ });
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/ChecksMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/ChecksMediumTest.java
new file mode 100644
index 00000000000..804a66a7d4d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/ChecksMediumTest.java
@@ -0,0 +1,127 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issues;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.batch.rule.LoadedActiveRule;
+import org.sonar.scanner.protocol.output.ScannerReport.Issue;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ChecksMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addRule("TemplateRule_1234", "xoo", "TemplateRule_1234", "A template rule")
+ .addRule("TemplateRule_1235", "xoo", "TemplateRule_1235", "Another template rule")
+ .activateRule(createActiveRuleWithParam("xoo", "TemplateRule_1234", "TemplateRule", "A template rule", "MAJOR", null, "xoo", "line", "1"))
+ .activateRule(createActiveRuleWithParam("xoo", "TemplateRule_1235", "TemplateRule", "Another template rule", "MAJOR", null, "xoo", "line", "2"))
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testCheckWithTemplate() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "foo\nbar");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("src/sample.xoo"));
+ assertThat(issues).hasSize(2);
+
+ boolean foundIssueAtLine1 = false;
+ boolean foundIssueAtLine2 = false;
+ for (Issue issue : issues) {
+ if (issue.getLine() == 1) {
+ foundIssueAtLine1 = true;
+ assertThat(issue.getMsg()).isEqualTo("A template rule");
+ }
+ if (issue.getLine() == 2) {
+ foundIssueAtLine2 = true;
+ assertThat(issue.getMsg()).isEqualTo("Another template rule");
+ }
+ }
+ assertThat(foundIssueAtLine1).isTrue();
+ assertThat(foundIssueAtLine2).isTrue();
+ }
+
+ private LoadedActiveRule createActiveRuleWithParam(String repositoryKey, String ruleKey, @Nullable String templateRuleKey, String name, @Nullable String severity,
+ @Nullable String internalKey, @Nullable String languag, String paramKey, String paramValue) {
+ LoadedActiveRule r = new LoadedActiveRule();
+
+ r.setInternalKey(internalKey);
+ r.setRuleKey(RuleKey.of(repositoryKey, ruleKey));
+ r.setName(name);
+ r.setTemplateRuleKey(templateRuleKey);
+ r.setLanguage(languag);
+ r.setSeverity(severity);
+
+ Map<String, String> params = new HashMap<>();
+ params.put(paramKey, paramValue);
+ r.setParams(params);
+ return r;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesIssuesModeMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesIssuesModeMediumTest.java
new file mode 100644
index 00000000000..cae361869cb
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesIssuesModeMediumTest.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issues;
+
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.bootstrapper.IssueListener;
+import org.junit.After;
+import org.junit.Before;
+import com.google.common.collect.ImmutableMap;
+import org.sonar.api.CoreProperties;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+import org.apache.commons.io.FileUtils;
+import org.junit.Test;
+import org.sonar.batch.mediumtest.TaskResult;
+
+import java.io.File;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssuesIssuesModeMediumTest {
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester testerPreview = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addRules(new XooRulesDefinition())
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES))
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "OneIssuePerLine.internal", "xoo")
+ .setLastBuildDate(new Date())
+ .build();
+
+ @Before
+ public void prepare() {
+ testerPreview.start();
+ }
+
+ @After
+ public void stop() {
+ testerPreview.stop();
+ }
+
+ @Test
+ public void testIssueCallback() throws Exception {
+ File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
+ File tmpDir = temp.getRoot();
+ FileUtils.copyDirectory(projectDir, tmpDir);
+ IssueRecorder issueListener = new IssueRecorder();
+
+ TaskResult result1 = testerPreview
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .setIssueListener(issueListener)
+ .property(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES)
+ .start();
+
+ assertThat(result1.trackedIssues()).hasSize(14);
+ assertThat(issueListener.issueList).hasSize(14);
+ issueListener = new IssueRecorder();
+
+ TaskResult result2 = testerPreview
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .setIssueListener(issueListener)
+ .property(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES)
+ .start();
+
+ assertThat(result2.trackedIssues()).hasSize(14);
+ assertThat(issueListener.issueList).hasSize(14);
+ }
+
+ private class IssueRecorder implements IssueListener {
+ List<Issue> issueList = new LinkedList<>();
+
+ @Override
+ public void handle(Issue issue) {
+ issueList.add(issue);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java
new file mode 100644
index 00000000000..50823ff4f48
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java
@@ -0,0 +1,187 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issues;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.bootstrapper.IssueListener;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.output.ScannerReport.Issue;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssuesMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addRules(new XooRulesDefinition())
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "OneIssuePerLine.internal", "xoo")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testNoIssueCallbackInNonPreview() throws Exception {
+ File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
+ File tmpDir = temp.getRoot();
+ FileUtils.copyDirectory(projectDir, tmpDir);
+ IssueRecorder issueListener = new IssueRecorder();
+
+ TaskResult result = tester
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .setIssueListener(issueListener)
+ .start();
+
+ assertThat(result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"))).hasSize(8);
+ assertThat(issueListener.issueList).hasSize(0);
+ }
+
+ @Test
+ public void testOneIssuePerLine() throws Exception {
+ File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
+ File tmpDir = temp.newFolder();
+ FileUtils.copyDirectory(projectDir, tmpDir);
+
+ TaskResult result = tester
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .start();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+ assertThat(issues).hasSize(8 /* lines */);
+
+ Issue issue = issues.get(0);
+ assertThat(issue.getTextRange().getStartLine()).isEqualTo(issue.getLine());
+ assertThat(issue.getTextRange().getEndLine()).isEqualTo(issue.getLine());
+ }
+
+ @Test
+ public void findActiveRuleByInternalKey() throws Exception {
+ File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
+ File tmpDir = temp.newFolder();
+ FileUtils.copyDirectory(projectDir, tmpDir);
+
+ TaskResult result = tester
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .property("sonar.xoo.internalKey", "OneIssuePerLine.internal")
+ .start();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+ assertThat(issues).hasSize(8 /* lines */ + 1 /* file */);
+ }
+
+ @Test
+ public void testOverrideQProfileSeverity() throws Exception {
+ File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
+ File tmpDir = temp.newFolder();
+ FileUtils.copyDirectory(projectDir, tmpDir);
+
+ TaskResult result = tester
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .property("sonar.oneIssuePerLine.forceSeverity", "CRITICAL")
+ .start();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
+ assertThat(issues.get(0).getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL);
+ }
+
+ @Test
+ public void testIssueExclusion() throws Exception {
+ File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI());
+ File tmpDir = temp.newFolder();
+ FileUtils.copyDirectory(projectDir, tmpDir);
+
+ TaskResult result = tester
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .property("sonar.issue.ignore.allfile", "1")
+ .property("sonar.issue.ignore.allfile.1.fileRegexp", "object")
+ .start();
+
+ assertThat(result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"))).hasSize(8 /* lines */);
+ assertThat(result.issuesFor(result.inputFile("xources/hello/helloscala.xoo"))).isEmpty();
+ }
+
+ @Test
+ public void testIssueDetails() throws IOException {
+
+ File baseDir = temp.newFolder();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ List<Issue> issues = result.issuesFor(result.inputFile("src/sample.xoo"));
+ assertThat(issues).hasSize(10);
+
+ boolean foundIssueAtLine1 = false;
+ for (Issue issue : issues) {
+ if (issue.getLine() == 1) {
+ foundIssueAtLine1 = true;
+ assertThat(issue.getMsg()).isEqualTo("This issue is generated on each line");
+ assertThat(issue.hasGap()).isFalse();
+ }
+ }
+ assertThat(foundIssueAtLine1).isTrue();
+ }
+
+ private class IssueRecorder implements IssueListener {
+ List<Issue> issueList = new LinkedList<>();
+
+ @Override
+ public void handle(Issue issue) {
+ issueList.add(issue);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnDirMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnDirMediumTest.java
new file mode 100644
index 00000000000..aae11ca0e39
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnDirMediumTest.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issues;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssuesOnDirMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addRules(new XooRulesDefinition())
+ .addActiveRule("xoo", "OneIssueOnDirPerFile", null, "One issue per line", "MINOR", "xoo", "xoo")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void scanTempProject() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile1 = new File(srcDir, "sample1.xoo");
+ FileUtils.write(xooFile1, "Sample1 xoo\ncontent");
+
+ File xooFile2 = new File(srcDir, "sample2.xoo");
+ FileUtils.write(xooFile2, "Sample2 xoo\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ assertThat(result.issuesFor(result.inputDir("src"))).hasSize(2);
+ }
+
+ @Test
+ public void issueOnRootFolder() throws IOException {
+
+ File baseDir = temp.getRoot();
+
+ File xooFile1 = new File(baseDir, "sample1.xoo");
+ FileUtils.write(xooFile1, "Sample1 xoo\ncontent");
+
+ File xooFile2 = new File(baseDir, "sample2.xoo");
+ FileUtils.write(xooFile2, "Sample2 xoo\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", ".")
+ .build())
+ .start();
+
+ assertThat(result.issuesFor(result.inputDir(""))).hasSize(2);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnModuleMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnModuleMediumTest.java
new file mode 100644
index 00000000000..73434ed872d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/IssuesOnModuleMediumTest.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issues;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssuesOnModuleMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addRules(new XooRulesDefinition())
+ .addActiveRule("xoo", "OneIssuePerModule", null, "One issue per module", "MINOR", "xoo", "xoo")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void scanTempProject() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile1 = new File(srcDir, "sample1.xoo");
+ FileUtils.write(xooFile1, "Sample1 xoo\ncontent");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ assertThat(result.issuesFor(result.getReportComponent("com.foo.project"))).hasSize(1);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/MultilineIssuesMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/MultilineIssuesMediumTest.java
new file mode 100644
index 00000000000..7ec0fe90535
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issues/MultilineIssuesMediumTest.java
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issues;
+
+import java.io.File;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.output.ScannerReport.Flow;
+import org.sonar.scanner.protocol.output.ScannerReport.Issue;
+import org.sonar.scanner.protocol.output.ScannerReport.IssueLocation;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MultilineIssuesMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addActiveRule("xoo", "MultilineIssue", null, "Multinile Issue", "MAJOR", null, "xoo")
+ .build();
+
+ private TaskResult result;
+
+ @Before
+ public void prepare() throws Exception {
+ tester.start();
+
+ File projectDir = new File(MultilineIssuesMediumTest.class.getResource("/mediumtest/xoo/sample-multiline").toURI());
+ File tmpDir = temp.getRoot();
+ FileUtils.copyDirectory(projectDir, tmpDir);
+
+ result = tester
+ .newScanTask(new File(tmpDir, "sonar-project.properties"))
+ .start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testIssueRange() throws Exception {
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/Single.xoo"));
+ assertThat(issues).hasSize(1);
+ Issue issue = issues.get(0);
+ assertThat(issue.getLine()).isEqualTo(6);
+ assertThat(issue.getMsg()).isEqualTo("Primary location");
+ assertThat(issue.getTextRange().getStartLine()).isEqualTo(6);
+ assertThat(issue.getTextRange().getStartOffset()).isEqualTo(23);
+ assertThat(issue.getTextRange().getEndLine()).isEqualTo(6);
+ assertThat(issue.getTextRange().getEndOffset()).isEqualTo(50);
+ }
+
+ @Test
+ public void testMultilineIssueRange() throws Exception {
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/Multiline.xoo"));
+ assertThat(issues).hasSize(1);
+ Issue issue = issues.get(0);
+ assertThat(issue.getLine()).isEqualTo(6);
+ assertThat(issue.getMsg()).isEqualTo("Primary location");
+ assertThat(issue.getTextRange().getStartLine()).isEqualTo(6);
+ assertThat(issue.getTextRange().getStartOffset()).isEqualTo(23);
+ assertThat(issue.getTextRange().getEndLine()).isEqualTo(7);
+ assertThat(issue.getTextRange().getEndOffset()).isEqualTo(23);
+ }
+
+ @Test
+ public void testFlowWithSingleLocation() throws Exception {
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/Multiple.xoo"));
+ assertThat(issues).hasSize(1);
+ Issue issue = issues.get(0);
+ assertThat(issue.getLine()).isEqualTo(6);
+ assertThat(issue.getMsg()).isEqualTo("Primary location");
+ assertThat(issue.getTextRange().getStartLine()).isEqualTo(6);
+ assertThat(issue.getTextRange().getStartOffset()).isEqualTo(23);
+ assertThat(issue.getTextRange().getEndLine()).isEqualTo(6);
+ assertThat(issue.getTextRange().getEndOffset()).isEqualTo(50);
+
+ assertThat(issue.getFlowList()).hasSize(1);
+ Flow flow = issue.getFlow(0);
+ assertThat(flow.getLocationList()).hasSize(1);
+ IssueLocation additionalLocation = flow.getLocation(0);
+ assertThat(additionalLocation.getMsg()).isEqualTo("Flow step #1");
+ assertThat(additionalLocation.getTextRange().getStartLine()).isEqualTo(7);
+ assertThat(additionalLocation.getTextRange().getStartOffset()).isEqualTo(26);
+ assertThat(additionalLocation.getTextRange().getEndLine()).isEqualTo(7);
+ assertThat(additionalLocation.getTextRange().getEndOffset()).isEqualTo(53);
+ }
+
+ @Test
+ public void testFlowsWithMultipleElements() throws Exception {
+ List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/WithFlow.xoo"));
+ assertThat(issues).hasSize(1);
+ Issue issue = issues.get(0);
+ assertThat(issue.getFlowList()).hasSize(1);
+
+ Flow flow = issue.getFlow(0);
+ assertThat(flow.getLocationList()).hasSize(2);
+ // TODO more assertions
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/EmptyFileTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/EmptyFileTest.java
new file mode 100644
index 00000000000..22eb34b4c22
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/EmptyFileTest.java
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issuesmode;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import org.apache.commons.io.filefilter.FileFilterUtils;
+import org.apache.commons.io.FileUtils;
+import org.sonar.xoo.rule.XooRulesDefinition;
+import com.google.common.collect.ImmutableMap;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+
+import java.io.File;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EmptyFileTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES))
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "my/internal/key", "xoo")
+ .setPreviousAnalysisDate(new Date())
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testIssueTrackingWithIssueOnEmptyFile() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample-with-empty-file");
+
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.xoo.internalKey", "my/internal/key")
+ .start();
+
+ for(TrackedIssue i : result.trackedIssues()) {
+ System.out.println(i.startLine() + " " + i.getMessage());
+ }
+
+ assertThat(result.trackedIssues()).hasSize(11);
+ }
+
+ private File copyProject(String path) throws Exception {
+ File projectDir = temp.newFolder();
+ File originalProjectDir = new File(EmptyFileTest.class.getResource(path).toURI());
+ FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar")));
+ return projectDir;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/IssueModeAndReportsMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/IssueModeAndReportsMediumTest.java
new file mode 100644
index 00000000000..e02c84ec71a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/IssueModeAndReportsMediumTest.java
@@ -0,0 +1,316 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issuesmode;
+
+import org.apache.commons.lang.StringUtils;
+
+import org.sonar.api.utils.log.LoggerLevel;
+import org.assertj.core.api.Condition;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import com.google.common.collect.ImmutableMap;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.FileFilterUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.bootstrapper.IssueListener;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.batch.scan.report.ConsoleReport;
+import org.sonar.scanner.protocol.Constants.Severity;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueModeAndReportsMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @org.junit.Rule
+ public LogTester logTester = new LogTester();
+
+ private static SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
+
+ private static Long date(String date) {
+ try {
+ return sdf.parse(date).getTime();
+ } catch (ParseException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES))
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addRules(new XooRulesDefinition())
+ .addRule("manual:MyManualIssue", "manual", "MyManualIssue", "My manual issue")
+ .addRule("manual:MyManualIssueDup", "manual", "MyManualIssue", "My manual issue")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", null, "xoo")
+ .addActiveRule("xoo", "OneIssueOnDirPerFile", null, "OneIssueOnDirPerFile", "MAJOR", null, "xoo")
+ .addActiveRule("xoo", "OneIssuePerModule", null, "OneIssuePerModule", "MAJOR", null, "xoo")
+ .addActiveRule("manual", "MyManualIssue", null, "My manual issue", "MAJOR", null, null)
+ .setPreviousAnalysisDate(new Date())
+ // Existing issue that is still detected
+ .mockServerIssue(ServerIssue.newBuilder().setKey("xyz")
+ .setModuleKey("sample")
+ .setPath("xources/hello/HelloJava.xoo")
+ .setRuleRepository("xoo")
+ .setRuleKey("OneIssuePerLine")
+ .setLine(1)
+ .setSeverity(Severity.MAJOR)
+ .setCreationDate(date("14/03/2004"))
+ .setChecksum(DigestUtils.md5Hex("packagehello;"))
+ .setStatus("OPEN")
+ .build())
+ // Existing issue that is no more detected (will be closed)
+ .mockServerIssue(ServerIssue.newBuilder().setKey("resolved")
+ .setModuleKey("sample")
+ .setPath("xources/hello/HelloJava.xoo")
+ .setRuleRepository("xoo")
+ .setRuleKey("OneIssuePerLine")
+ .setLine(1)
+ .setSeverity(Severity.MAJOR)
+ .setCreationDate(date("14/03/2004"))
+ .setChecksum(DigestUtils.md5Hex("dontexist"))
+ .setStatus("OPEN")
+ .build())
+ // Existing issue on project that is still detected
+ .mockServerIssue(ServerIssue.newBuilder().setKey("resolved-on-project")
+ .setModuleKey("sample")
+ .setRuleRepository("xoo")
+ .setRuleKey("OneIssuePerModule")
+ .setSeverity(Severity.CRITICAL)
+ .setCreationDate(date("14/03/2004"))
+ .setStatus("OPEN")
+ .build())
+ // Manual issue
+ .mockServerIssue(ServerIssue.newBuilder().setKey("manual")
+ .setModuleKey("sample")
+ .setPath("xources/hello/HelloJava.xoo")
+ .setRuleRepository("manual")
+ .setRuleKey("MyManualIssue")
+ .setLine(1)
+ .setSeverity(Severity.MAJOR)
+ .setCreationDate(date("14/03/2004"))
+ .setChecksum(DigestUtils.md5Hex("packagehello;"))
+ .setStatus("OPEN")
+ .build())
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ private File copyProject(String path) throws Exception {
+ File projectDir = temp.newFolder();
+ File originalProjectDir = new File(IssueModeAndReportsMediumTest.class.getResource(path).toURI());
+ FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar")));
+ return projectDir;
+ }
+
+ @Test
+ public void testIssueTracking() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .start();
+
+ int newIssues = 0;
+ int openIssues = 0;
+ int resolvedIssue = 0;
+ for (TrackedIssue issue : result.trackedIssues()) {
+ System.out
+ .println(issue.getMessage() + " " + issue.key() + " " + issue.getRuleKey() + " " + issue.isNew() + " " + issue.resolution() + " " + issue.componentKey() + " "
+ + issue.startLine());
+ if (issue.isNew()) {
+ newIssues++;
+ } else if (issue.resolution() != null) {
+ resolvedIssue++;
+ } else {
+ openIssues++;
+ }
+ }
+ System.out.println("new: " + newIssues + " open: " + openIssues + " resolved " + resolvedIssue);
+ assertThat(newIssues).isEqualTo(16);
+ assertThat(openIssues).isEqualTo(3);
+ assertThat(resolvedIssue).isEqualTo(1);
+
+ // progress report
+ String logs = StringUtils.join(logTester.logs(LoggerLevel.INFO), "\n");
+
+ assertThat(logs).contains("Performing issue tracking");
+ assertThat(logs).contains("6/6 components tracked");
+
+ // assert that original fields of a matched issue are kept
+ assertThat(result.trackedIssues()).haveExactly(1, new Condition<TrackedIssue>() {
+ @Override
+ public boolean matches(TrackedIssue value) {
+ return value.isNew() == false
+ && "resolved-on-project".equals(value.key())
+ && "OPEN".equals(value.status())
+ && new Date(date("14/03/2004")).equals(value.creationDate());
+ }
+ });
+ }
+
+ @Test
+ public void testConsoleReport() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.issuesReport.console.enable", "true")
+ .start();
+
+ assertThat(getReportLog()).contains("+16 issues", "+16 major");
+ }
+
+ @Test
+ public void testPostJob() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.xoo.enablePostJob", "true")
+ .start();
+
+ assertThat(logTester.logs()).contains("Resolved issues: 1", "Open issues: 19");
+ }
+
+ private String getReportLog() {
+ for (String log : logTester.logs()) {
+ if (log.contains(ConsoleReport.HEADER)) {
+ return log;
+ }
+ }
+ throw new IllegalStateException("No console report");
+ }
+
+ @Test
+ public void testHtmlReport() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.issuesReport.html.enable", "true")
+ .start();
+
+ assertThat(new File(projectDir, ".sonar/issues-report/issues-report.html")).exists();
+ assertThat(new File(projectDir, ".sonar/issues-report/issues-report-light.html")).exists();
+ }
+
+ @Test
+ public void testHtmlReportNoFile() throws Exception {
+ File baseDir = temp.newFolder();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "sample")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.issuesReport.html.enable", "true")
+ .build())
+ .start();
+
+ assertThat(FileUtils.readFileToString(new File(baseDir, ".sonar/issues-report/issues-report.html"))).contains("No file analyzed");
+ assertThat(FileUtils.readFileToString(new File(baseDir, ".sonar/issues-report/issues-report-light.html"))).contains("No file analyzed");
+ }
+
+ @Test
+ public void testIssueCallback() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+ IssueRecorder issueListener = new IssueRecorder();
+
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .setIssueListener(issueListener)
+ .start();
+
+ assertThat(result.trackedIssues()).hasSize(20);
+ assertThat(issueListener.issueList).hasSize(20);
+ }
+
+ private class IssueRecorder implements IssueListener {
+ List<Issue> issueList = new LinkedList<>();
+
+ @Override
+ public void handle(Issue issue) {
+ issueList.add(issue);
+ }
+ }
+
+ @Test
+ public void noSyntaxHighlightingInIssuesMode() throws IOException {
+
+ File baseDir = temp.newFolder();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xoohighlightingFile = new File(srcDir, "sample.xoo.highlighting");
+ FileUtils.write(xooFile, "Sample xoo\ncontent plop");
+ FileUtils.write(xoohighlightingFile, "0:10:s\n11:18:k");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ assertThat(result.getReportReader().hasSyntaxHighlighting(1)).isFalse();
+ assertThat(result.getReportReader().hasSyntaxHighlighting(2)).isFalse();
+ assertThat(result.getReportReader().hasSyntaxHighlighting(3)).isFalse();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NoPreviousAnalysisTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NoPreviousAnalysisTest.java
new file mode 100644
index 00000000000..fa133979562
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NoPreviousAnalysisTest.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issuesmode;
+
+import org.sonar.batch.mediumtest.TaskResult;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.FileFilterUtils;
+
+import java.io.File;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+public class NoPreviousAnalysisTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES))
+ .registerPlugin("xoo", new XooPlugin())
+ .addRules(new XooRulesDefinition())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "my/internal/key", "xoo")
+ .setPreviousAnalysisDate(null)
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testIssueTrackingWithIssueOnEmptyFile() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .start();
+
+ assertThat(result.trackedIssues()).hasSize(14);
+
+ }
+
+ private File copyProject(String path) throws Exception {
+ File projectDir = temp.newFolder();
+ File originalProjectDir = new File(IssueModeAndReportsMediumTest.class.getResource(path).toURI());
+ FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar")));
+ return projectDir;
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NonAssociatedProject.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NonAssociatedProject.java
new file mode 100644
index 00000000000..32c66f2689f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/NonAssociatedProject.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issuesmode;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.FileFilterUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+
+import java.io.File;
+import java.io.IOException;
+
+public class NonAssociatedProject {
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @org.junit.Rule
+ public LogTester logTester = new LogTester();
+
+ public BatchMediumTester tester;
+
+ @Before
+ public void prepare() throws IOException {
+ tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(
+ CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES,
+ CoreProperties.GLOBAL_WORKING_DIRECTORY, temp.newFolder().getAbsolutePath()))
+ .registerPlugin("xoo", new XooPlugin())
+ .addQProfile("xoo", "Sonar Way")
+ .addRules(new XooRulesDefinition())
+ .addRule("manual:MyManualIssue", "manual", "MyManualIssue", "My manual issue")
+ .addRule("manual:MyManualIssueDup", "manual", "MyManualIssue", "My manual issue")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", null, "xoo")
+ .addActiveRule("xoo", "OneIssueOnDirPerFile", null, "OneIssueOnDirPerFile", "MAJOR", null, "xoo")
+ .addActiveRule("xoo", "OneIssuePerModule", null, "OneIssuePerModule", "MAJOR", null, "xoo")
+ .addActiveRule("manual", "MyManualIssue", null, "My manual issue", "MAJOR", null, null)
+ .setAssociated(false)
+ .build();
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ private File copyProject(String path) throws Exception {
+ File projectDir = temp.newFolder();
+ File originalProjectDir = new File(IssueModeAndReportsMediumTest.class.getResource(path).toURI());
+ FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar")));
+ return projectDir;
+ }
+
+ @Test
+ public void testNonAssociated() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/multi-modules-sample-not-associated");
+
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .start();
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/ScanOnlyChangedTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/ScanOnlyChangedTest.java
new file mode 100644
index 00000000000..eb5850b0809
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/issuesmode/ScanOnlyChangedTest.java
@@ -0,0 +1,212 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.issuesmode;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import org.assertj.core.api.Condition;
+import com.google.common.io.Resources;
+import org.sonar.batch.repository.FileData;
+import org.sonar.scanner.protocol.Constants.Severity;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.filefilter.FileFilterUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.sonar.api.CoreProperties;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.xoo.XooPlugin;
+import org.sonar.xoo.rule.XooRulesDefinition;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.utils.log.LogTester;
+import org.junit.Test;
+import org.sonar.batch.mediumtest.TaskResult;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ScanOnlyChangedTest {
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @org.junit.Rule
+ public LogTester logTester = new LogTester();
+
+ private static SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
+
+ private BatchMediumTester tester;
+
+ private static Long date(String date) {
+ try {
+ return sdf.parse(date).getTime();
+ } catch (ParseException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Before
+ public void prepare() throws IOException {
+ String filePath = "xources/hello/HelloJava.xoo";
+ String md5sum = DigestUtils.md5Hex(FileUtils.readFileToString(new File(
+ Resources.getResource("mediumtest/xoo/sample/" + filePath).getPath())));
+
+ tester = BatchMediumTester.builder()
+ .bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES))
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addRules(new XooRulesDefinition())
+ .addRule("manual:MyManualIssue", "manual", "MyManualIssue", "My manual issue")
+ .addRule("manual:MyManualIssueDup", "manual", "MyManualIssue", "My manual issue")
+ .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", null, "xoo")
+ .addActiveRule("xoo", "OneIssueOnDirPerFile", null, "OneIssueOnDirPerFile", "MAJOR", null, "xoo")
+ .addActiveRule("xoo", "OneIssuePerModule", null, "OneIssuePerModule", "MAJOR", null, "xoo")
+ .addActiveRule("manual", "MyManualIssue", null, "My manual issue", "MAJOR", null, null)
+ // this will cause the file to have status==SAME
+ .addFileData("sample", filePath, new FileData(md5sum, null))
+ .setPreviousAnalysisDate(new Date())
+ // Existing issue that is copied
+ .mockServerIssue(ServerIssue.newBuilder().setKey("xyz")
+ .setModuleKey("sample")
+ .setMsg("One issue per Line copied")
+ .setPath("xources/hello/HelloJava.xoo")
+ .setRuleRepository("xoo")
+ .setRuleKey("OneIssuePerLine")
+ .setLine(1)
+ .setSeverity(Severity.MAJOR)
+ .setCreationDate(date("14/03/2004"))
+ .setChecksum(DigestUtils.md5Hex("packagehello;"))
+ .setStatus("OPEN")
+ .build())
+ // Existing issue on project that is still detected
+ .mockServerIssue(ServerIssue.newBuilder().setKey("resolved-on-project")
+ .setModuleKey("sample")
+ .setRuleRepository("xoo")
+ .setRuleKey("OneIssuePerModule")
+ .setSeverity(Severity.CRITICAL)
+ .setCreationDate(date("14/03/2004"))
+ .setStatus("OPEN")
+ .build())
+ // Manual issue
+ .mockServerIssue(ServerIssue.newBuilder().setKey("manual")
+ .setModuleKey("sample")
+ .setPath("xources/hello/HelloJava.xoo")
+ .setRuleRepository("manual")
+ .setRuleKey("MyManualIssue")
+ .setLine(1)
+ .setSeverity(Severity.MAJOR)
+ .setCreationDate(date("14/03/2004"))
+ .setChecksum(DigestUtils.md5Hex("packagehello;"))
+ .setStatus("OPEN")
+ .build())
+ .build();
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ private File copyProject(String path) throws Exception {
+ File projectDir = temp.newFolder();
+ File originalProjectDir = new File(IssueModeAndReportsMediumTest.class.getResource(path).toURI());
+ FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar")));
+ return projectDir;
+ }
+
+ @Test
+ public void testScanAll() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .property("sonar.scanAllFiles", "true")
+ .start();
+
+ assertNumberIssues(result, 16, 3, 0);
+
+ /*
+ * 8 new per line
+ * 1 manual
+ */
+ assertNumberIssuesOnFile(result, "HelloJava.xoo", 9);
+ }
+
+ @Test
+ public void testScanOnlyChangedFiles() throws Exception {
+ File projectDir = copyProject("/mediumtest/xoo/sample");
+
+ TaskResult result = tester
+ .newScanTask(new File(projectDir, "sonar-project.properties"))
+ .start();
+
+ /*
+ * We have:
+ * 6 new issues per line (open) in helloscala.xoo
+ * 2 new issues per file in helloscala.xoo / ClassOneTest.xoo
+ * 1 server issue (open, not new) copied from server in HelloJava.xoo (this file is unchanged)
+ * 1 manual issue (open, not new) in HelloJava.xoo
+ * 1 existing issue on the project (open, not new)
+ */
+ assertNumberIssues(result, 8, 3, 0);
+
+ // should only have server issues (HelloJava.xoo should not have been analyzed)
+ assertNumberIssuesOnFile(result, "HelloJava.xoo", 2);
+ }
+
+ private static void assertNumberIssuesOnFile(TaskResult result, final String fileNameEndsWith, int issues) {
+ assertThat(result.trackedIssues()).haveExactly(issues, new Condition<TrackedIssue>() {
+ @Override
+ public boolean matches(TrackedIssue value) {
+ return value.componentKey().endsWith(fileNameEndsWith);
+ }
+ });
+ }
+
+ private static void assertNumberIssues(TaskResult result, int expectedNew, int expectedOpen, int expectedResolved) {
+ int newIssues = 0;
+ int openIssues = 0;
+ int resolvedIssue = 0;
+ for (TrackedIssue issue : result.trackedIssues()) {
+ System.out
+ .println(issue.getMessage() + " " + issue.key() + " " + issue.getRuleKey() + " " + issue.isNew() + " " + issue.resolution() + " " + issue.componentKey() + " "
+ + issue.startLine());
+ if (issue.isNew()) {
+ newIssues++;
+ } else if (issue.resolution() != null) {
+ resolvedIssue++;
+ } else {
+ openIssues++;
+ }
+ }
+
+ System.out.println("new: " + newIssues + " open: " + openIssues + " resolved " + resolvedIssue);
+ assertThat(newIssues).isEqualTo(expectedNew);
+ assertThat(openIssues).isEqualTo(expectedOpen);
+ assertThat(resolvedIssue).isEqualTo(expectedResolved);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/ExceptionHandlingMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/ExceptionHandlingMediumTest.java
new file mode 100644
index 00000000000..db1a2dcd30e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/ExceptionHandlingMediumTest.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.log;
+
+import java.util.Collections;
+
+import org.hamcrest.Matchers;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.BeforeClass;
+import org.sonar.batch.bootstrapper.EnvironmentInformation;
+import org.sonar.api.utils.MessageException;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonar.batch.repository.GlobalRepositoriesLoader;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+import org.sonar.batch.bootstrapper.Batch;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class ExceptionHandlingMediumTest {
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private Batch batch;
+ private static ErrorGlobalRepositoriesLoader loader;
+
+ @BeforeClass
+ public static void beforeClass() {
+ loader = new ErrorGlobalRepositoriesLoader();
+ }
+
+ public void setUp(boolean verbose) {
+ Batch.Builder builder = Batch.builder()
+ .setEnableLoggingConfiguration(true)
+ .addComponents(
+ loader,
+ new EnvironmentInformation("mediumTest", "1.0"));
+
+ if (verbose) {
+ builder.setBootstrapProperties(Collections.singletonMap("sonar.verbose", "true"));
+ }
+ batch = builder.build();
+ }
+
+ @Test
+ public void test() throws Exception {
+ setUp(false);
+ loader.withCause = false;
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("Error loading repository");
+ thrown.expectCause(Matchers.nullValue(Throwable.class));
+
+ batch.start();
+ }
+
+ @Test
+ public void testWithCause() throws Exception {
+ setUp(false);
+ loader.withCause = true;
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("Error loading repository");
+ thrown.expectCause(new TypeSafeMatcher<Throwable>() {
+ @Override
+ public void describeTo(Description description) {
+ }
+
+ @Override
+ protected boolean matchesSafely(Throwable item) {
+ return item instanceof IllegalStateException && item.getMessage().equals("Code 401");
+ }
+ });
+
+ batch.start();
+ }
+
+ @Test
+ public void testWithVerbose() throws Exception {
+ setUp(true);
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Unable to load component class");
+ batch.start();
+ }
+
+ private static class ErrorGlobalRepositoriesLoader implements GlobalRepositoriesLoader {
+ boolean withCause = false;
+
+ @Override
+ public GlobalRepositories load(MutableBoolean fromCache) {
+ if (withCause) {
+ IllegalStateException cause = new IllegalStateException("Code 401");
+ throw MessageException.of("Error loading repository", cause);
+ } else {
+ throw MessageException.of("Error loading repository");
+ }
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/LogListenerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/LogListenerTest.java
new file mode 100644
index 00000000000..f360dca56ac
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/log/LogListenerTest.java
@@ -0,0 +1,216 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.log;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.batch.bootstrapper.LogOutput;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.xoo.XooPlugin;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LogListenerTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private Pattern simpleTimePattern = Pattern.compile("\\d{2}:\\d{2}:\\d{2}");
+ private List<LogEvent> logOutput;
+ private StringBuilder logOutputStr;
+ private ByteArrayOutputStream stdOutTarget = new ByteArrayOutputStream();
+ private ByteArrayOutputStream stdErrTarget = new ByteArrayOutputStream();
+ private static PrintStream savedStdOut;
+ private static PrintStream savedStdErr;
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .setLogOutput(new SimpleLogListener())
+ .build();
+
+ private File baseDir;
+
+ private ImmutableMap.Builder<String, String> builder;
+
+ @BeforeClass
+ public static void backupStdStreams() {
+ savedStdOut = System.out;
+ savedStdErr = System.err;
+ }
+
+ @AfterClass
+ public static void resumeStdStreams() {
+ if (savedStdOut != null) {
+ System.setOut(savedStdOut);
+ }
+ if (savedStdErr != null) {
+ System.setErr(savedStdErr);
+ }
+ }
+
+ @Before
+ public void prepare() throws IOException {
+ System.setOut(new PrintStream(stdOutTarget));
+ System.setErr(new PrintStream(stdErrTarget));
+ // logger from the batch might write to it asynchronously
+ logOutput = Collections.synchronizedList(new LinkedList<LogEvent>());
+ logOutputStr = new StringBuilder();
+ tester.start();
+
+ baseDir = temp.getRoot();
+
+ builder = ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project");
+ }
+
+ private void assertNoStdOutput() {
+ assertThat(stdOutTarget.toByteArray()).isEmpty();
+ assertThat(stdErrTarget.toByteArray()).isEmpty();
+ }
+
+ /**
+ * Check that log message is not formatted, i.e. has no log level and timestamp.
+ */
+ private void assertMsgClean(String msg) {
+ for (LogOutput.Level l : LogOutput.Level.values()) {
+ assertThat(msg).doesNotContain(l.toString());
+ }
+
+ Matcher matcher = simpleTimePattern.matcher(msg);
+ assertThat(matcher.find()).isFalse();
+ }
+
+ @Test
+ public void testChangeLogForAnalysis() throws IOException, InterruptedException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.verbose", "true")
+ .build())
+ .start();
+
+ tester.stop();
+ for (LogEvent e : logOutput) {
+ savedStdOut.println("[captured]" + e.level + " " + e.msg);
+ }
+
+ // only done in DEBUG during analysis
+ assertThat(logOutputStr.toString()).contains("Post-jobs : ");
+ }
+
+ @Test
+ public void testNoStdLog() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+ tester.stop();
+
+ assertNoStdOutput();
+ assertThat(logOutput).isNotEmpty();
+
+ synchronized (logOutput) {
+ for (LogEvent e : logOutput) {
+ savedStdOut.println("[captured]" + e.level + " " + e.msg);
+ }
+ }
+ }
+
+ @Test
+ public void testNoFormattedMsgs() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+
+ tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+ tester.stop();
+
+ assertNoStdOutput();
+
+ synchronized (logOutput) {
+ for (LogEvent e : logOutput) {
+ assertMsgClean(e.msg);
+ savedStdOut.println("[captured]" + e.level + " " + e.msg);
+ }
+ }
+ }
+
+ private class SimpleLogListener implements LogOutput {
+ @Override
+ public void log(String msg, Level level) {
+ logOutput.add(new LogEvent(msg, level));
+ logOutputStr.append(msg).append("\n");
+ }
+ }
+
+ private static class LogEvent {
+ String msg;
+ LogOutput.Level level;
+
+ LogEvent(String msg, LogOutput.Level level) {
+ this.msg = msg;
+ this.level = level;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/measures/MeasuresMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/measures/MeasuresMediumTest.java
new file mode 100644
index 00000000000..e9db30c49f4
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/measures/MeasuresMediumTest.java
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.measures;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.assertj.core.groups.Tuple;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.output.ScannerReport.Measure;
+import org.sonar.xoo.XooPlugin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class MeasuresMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private File baseDir;
+ private File srcDir;
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Before
+ public void setUp() {
+ baseDir = temp.getRoot();
+ srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+ }
+
+ @Test
+ public void computeMeasuresOnTempProject() throws IOException {
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xooMeasureFile = new File(srcDir, "sample.xoo.measures");
+ FileUtils.write(xooFile, "Sample xoo\ncontent");
+ FileUtils.write(xooMeasureFile, "lines:20");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.cpd.xoo.skip", "true")
+ .build())
+ .start();
+
+ Map<String, List<Measure>> allMeasures = result.allMeasures();
+
+ assertThat(allMeasures.get("com.foo.project")).extracting("metricKey", "intValue", "doubleValue", "stringValue").containsOnly(
+ Tuple.tuple(CoreMetrics.QUALITY_PROFILES_KEY, 0, 0.0,
+ "[{\"key\":\"Sonar Way\",\"language\":\"xoo\",\"name\":\"Sonar Way\",\"rulesUpdatedAt\":\"2009-02-13T23:31:31+0000\"}]"));
+
+ assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey", "intValue").containsOnly(
+ Tuple.tuple(CoreMetrics.LINES_KEY, 2));
+ }
+
+ @Test
+ public void computeLinesOnAllFiles() throws IOException {
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "Sample xoo\n\ncontent");
+
+ File otherFile = new File(srcDir, "sample.other");
+ FileUtils.write(otherFile, "Sample other\ncontent\n");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.import_unknown_files", "true")
+ .build())
+ .start();
+
+ Map<String, List<Measure>> allMeasures = result.allMeasures();
+
+ assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey", "intValue")
+ .contains(tuple("lines", 3));
+ assertThat(allMeasures.get("com.foo.project:src/sample.other")).extracting("metricKey", "intValue")
+ .contains(tuple("lines", 3), tuple("ncloc", 2));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/scm/ScmMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/scm/ScmMediumTest.java
new file mode 100644
index 00000000000..37c8da1ef01
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/scm/ScmMediumTest.java
@@ -0,0 +1,363 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.scm;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.utils.PathUtils;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.BatchMediumTester.TaskBuilder;
+import org.sonar.batch.repository.FileData;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReport.Component;
+import org.sonar.scanner.protocol.output.ScannerReport.Changesets.Changeset;
+import org.sonar.xoo.XooPlugin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ScmMediumTest {
+
+ private static final String MISSING_BLAME_INFORMATION_FOR_THE_FOLLOWING_FILES = "Missing blame information for the following files:";
+ private static final String CHANGED_CONTENT_SCM_ON_SERVER_XOO = "src/changed_content_scm_on_server.xoo";
+ private static final String SAME_CONTENT_SCM_ON_SERVER_XOO = "src/same_content_scm_on_server.xoo";
+ private static final String SAME_CONTENT_NO_SCM_ON_SERVER_XOO = "src/same_content_no_scm_on_server.xoo";
+ private static final String SAMPLE_XOO_CONTENT = "Sample xoo\ncontent";
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .addFileData("com.foo.project", CHANGED_CONTENT_SCM_ON_SERVER_XOO, new FileData(DigestUtils.md5Hex(SAMPLE_XOO_CONTENT), null))
+ .addFileData("com.foo.project", SAME_CONTENT_NO_SCM_ON_SERVER_XOO, new FileData(DigestUtils.md5Hex(SAMPLE_XOO_CONTENT), null))
+ .addFileData("com.foo.project", SAME_CONTENT_SCM_ON_SERVER_XOO, new FileData(DigestUtils.md5Hex(SAMPLE_XOO_CONTENT), "1.1"))
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testScmMeasure() throws IOException {
+
+ File baseDir = prepareProject();
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.scm.provider", "xoo")
+ .build())
+ .start();
+
+ ScannerReport.Changesets fileScm = getChangesets(baseDir, "src/sample.xoo");
+
+ assertThat(fileScm.getChangesetIndexByLineList()).hasSize(5);
+
+ Changeset changesetLine1 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(0));
+ assertThat(changesetLine1.hasAuthor()).isFalse();
+
+ Changeset changesetLine2 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(1));
+ assertThat(changesetLine2.getAuthor()).isEqualTo("julien");
+
+ Changeset changesetLine3 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(2));
+ assertThat(changesetLine3.getAuthor()).isEqualTo("julien");
+
+ Changeset changesetLine4 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(3));
+ assertThat(changesetLine4.getAuthor()).isEqualTo("julien");
+
+ Changeset changesetLine5 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(4));
+ assertThat(changesetLine5.getAuthor()).isEqualTo("simon");
+ }
+
+ private ScannerReport.Changesets getChangesets(File baseDir, String path) {
+ File reportDir = new File(baseDir, ".sonar/batch-report");
+ ScannerReportReader reader = new ScannerReportReader(reportDir);
+
+ Component project = reader.readComponent(reader.readMetadata().getRootComponentRef());
+ Component dir = reader.readComponent(project.getChildRef(0));
+ for (Integer fileRef : dir.getChildRefList()) {
+ Component file = reader.readComponent(fileRef);
+ if (file.getPath().equals(path)) {
+ return reader.readChangesets(file.getRef());
+ }
+ }
+ return null;
+ }
+
+ @Test
+ public void noScmOnEmptyFile() throws IOException {
+
+ File baseDir = prepareProject();
+
+ // Clear file content
+ FileUtils.write(new File(baseDir, "src/sample.xoo"), "");
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.scm.provider", "xoo")
+ .build())
+ .start();
+
+ ScannerReport.Changesets changesets = getChangesets(baseDir, "src/sample.xoo");
+
+ assertThat(changesets).isNull();
+ }
+
+ @Test
+ public void log_files_with_missing_blame() throws IOException {
+
+ File baseDir = prepareProject();
+ File xooFileWithoutBlame = new File(baseDir, "src/sample_no_blame.xoo");
+ FileUtils.write(xooFileWithoutBlame, "Sample xoo\ncontent\n3\n4\n5");
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.scm.provider", "xoo")
+ .build())
+ .start();
+
+ ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo");
+ assertThat(file1Scm).isNotNull();
+
+ ScannerReport.Changesets fileWithoutBlameScm = getChangesets(baseDir, "src/sample_no_blame.xoo");
+ assertThat(fileWithoutBlameScm).isNull();
+
+ assertThat(logTester.logs()).containsSubsequence("2 files to be analyzed", "1/2 files analyzed", MISSING_BLAME_INFORMATION_FOR_THE_FOLLOWING_FILES,
+ " * " + PathUtils.sanitize(xooFileWithoutBlame.toPath().toString()));
+ }
+
+ // SONAR-6397
+ @Test
+ public void optimize_blame() throws IOException {
+
+ File baseDir = prepareProject();
+ File changedContentScmOnServer = new File(baseDir, CHANGED_CONTENT_SCM_ON_SERVER_XOO);
+ FileUtils.write(changedContentScmOnServer, SAMPLE_XOO_CONTENT + "\nchanged");
+ File xooScmFile = new File(baseDir, CHANGED_CONTENT_SCM_ON_SERVER_XOO + ".scm");
+ FileUtils.write(xooScmFile,
+ // revision,author,dateTime
+ "1,foo,2013-01-04\n" +
+ "1,bar,2013-01-04\n" +
+ "2,biz,2014-01-04\n");
+
+ File sameContentScmOnServer = new File(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO);
+ FileUtils.write(sameContentScmOnServer, SAMPLE_XOO_CONTENT);
+ // No need to write .scm file since this file should not be blamed
+
+ File sameContentNoScmOnServer = new File(baseDir, SAME_CONTENT_NO_SCM_ON_SERVER_XOO);
+ FileUtils.write(sameContentNoScmOnServer, SAMPLE_XOO_CONTENT);
+ xooScmFile = new File(baseDir, SAME_CONTENT_NO_SCM_ON_SERVER_XOO + ".scm");
+ FileUtils.write(xooScmFile,
+ // revision,author,dateTime
+ "1,foo,2013-01-04\n" +
+ "1,bar,2013-01-04\n");
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.scm.provider", "xoo")
+ .build())
+ .start();
+
+ assertThat(getChangesets(baseDir, "src/sample.xoo")).isNotNull();
+
+ assertThat(getChangesets(baseDir, CHANGED_CONTENT_SCM_ON_SERVER_XOO)).isNotNull();
+
+ assertThat(getChangesets(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO)).isNull();
+
+ assertThat(getChangesets(baseDir, SAME_CONTENT_NO_SCM_ON_SERVER_XOO)).isNotNull();
+
+ assertThat(logTester.logs()).containsSubsequence("3 files to be analyzed", "3/3 files analyzed");
+ assertThat(logTester.logs()).doesNotContain(MISSING_BLAME_INFORMATION_FOR_THE_FOLLOWING_FILES);
+ }
+
+ @Test
+ public void forceReload() throws IOException {
+
+ File baseDir = prepareProject();
+ File xooFileNoScm = new File(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO);
+ FileUtils.write(xooFileNoScm, SAMPLE_XOO_CONTENT);
+ File xooScmFile = new File(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO + ".scm");
+ FileUtils.write(xooScmFile,
+ // revision,author,dateTime
+ "1,foo,2013-01-04\n" +
+ "1,bar,2013-01-04\n");
+
+ TaskBuilder taskBuilder = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.scm.provider", "xoo")
+ // Force reload
+ .put("sonar.scm.forceReloadAll", "true")
+ .build());
+
+ taskBuilder.start();
+
+ ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo");
+ assertThat(file1Scm).isNotNull();
+
+ ScannerReport.Changesets file2Scm = getChangesets(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO);
+ assertThat(file2Scm).isNotNull();
+ }
+
+ @Test
+ public void configureUsingScmURL() throws IOException {
+
+ File baseDir = prepareProject();
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.links.scm_dev", "scm:xoo:foobar")
+ .build())
+ .start();
+
+ ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo");
+ assertThat(file1Scm).isNotNull();
+ }
+
+ @Test
+ public void testAutoDetection() throws IOException {
+
+ File baseDir = prepareProject();
+ new File(baseDir, ".xoo").createNewFile();
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo");
+ assertThat(file1Scm).isNotNull();
+ }
+
+ private File prepareProject() throws IOException {
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile1 = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile1, "Sample xoo\ncontent\n3\n4\n5");
+ File xooScmFile1 = new File(srcDir, "sample.xoo.scm");
+ FileUtils.write(xooScmFile1,
+ // revision,author,dateTime
+ "1,,2013-01-04\n" +
+ "2,julien,2013-01-04\n" +
+ "3,julien,2013-02-03\n" +
+ "3,julien,2013-02-03\n" +
+ "4,simon,2013-03-04\n");
+
+ return baseDir;
+ }
+
+ @Test
+ public void testDisableScmSensor() throws IOException {
+
+ File baseDir = prepareProject();
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.scm.disabled", "true")
+ .put("sonar.scm.provider", "xoo")
+ .put("sonar.cpd.xoo.skip", "true")
+ .build())
+ .start();
+
+ ScannerReport.Changesets changesets = getChangesets(baseDir, "src/sample.xoo");
+ assertThat(changesets).isNull();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/symbol/SymbolMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/symbol/SymbolMediumTest.java
new file mode 100644
index 00000000000..8d9836f994d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/symbol/SymbolMediumTest.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.symbol;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.xoo.XooPlugin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SymbolMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void computeSymbolReferencesOnTempProject() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xooSymbolFile = new File(srcDir, "sample.xoo.symbol");
+ FileUtils.write(xooFile, "Sample xoo\ncontent\nanother xoo");
+ // Highlight xoo symbol
+ FileUtils.write(xooSymbolFile, "7:10,27");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ InputFile file = result.inputFile("src/sample.xoo");
+ assertThat(result.symbolReferencesFor(file, 1, 7)).containsOnly(ScannerReport.TextRange.newBuilder().setStartLine(3).setStartOffset(8).setEndLine(3).setEndOffset(11).build());
+ }
+
+ @Test
+ public void computeSymbolReferencesWithVariableLength() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ File xooSymbolFile = new File(srcDir, "sample.xoo.symbol");
+ FileUtils.write(xooFile, "Sample xoo\ncontent\nanother xoo\nyet another");
+ // Highlight xoo symbol
+ FileUtils.write(xooSymbolFile, "7:10,27:32");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .build())
+ .start();
+
+ InputFile file = result.inputFile("src/sample.xoo");
+ assertThat(result.symbolReferencesFor(file, 1, 7)).containsOnly(ScannerReport.TextRange.newBuilder().setStartLine(3).setStartOffset(8).setEndLine(4).setEndOffset(1).build());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tasks/TasksMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tasks/TasksMediumTest.java
new file mode 100644
index 00000000000..766e9371898
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tasks/TasksMediumTest.java
@@ -0,0 +1,160 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.tasks;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.List;
+import org.assertj.core.api.Condition;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.SonarPlugin;
+import org.sonar.api.issue.action.Actions;
+import org.sonar.api.task.Task;
+import org.sonar.api.task.TaskDefinition;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.bootstrap.MockHttpServer;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TasksMediumTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("faketask", new FakeTaskPlugin())
+ .build();
+
+ private MockHttpServer server = null;
+
+ @After
+ public void stopServer() {
+ if (server != null) {
+ server.stop();
+ }
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void listTasksIncludingBroken() throws Exception {
+ tester.start();
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "list").build())
+ .start();
+
+ assertThat(logTester.logs()).haveExactly(1, new Condition<String>() {
+
+ @Override
+ public boolean matches(String value) {
+ return value.contains("Available tasks:") && value.contains("fake: Fake description") && value.contains("broken: Broken description");
+ }
+ });
+ }
+
+ @Test
+ public void runBroken() throws Exception {
+ tester.start();
+
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage(
+ "Unable to load component class org.sonar.batch.mediumtest.tasks.TasksMediumTest$BrokenTask");
+
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "broken").build())
+ .start();
+ }
+
+ @Test(expected = MessageException.class)
+ public void unsupportedTask() throws Exception {
+ tester = BatchMediumTester.builder()
+ .build();
+ tester.start();
+ tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "foo").build())
+ .start();
+ }
+
+ private void startServer(Integer responseStatus, String responseData) throws Exception {
+ server = new MockHttpServer();
+ server.start();
+
+ if (responseStatus != null) {
+ server.setMockResponseStatus(responseStatus);
+ }
+ if (responseData != null) {
+ server.setMockResponseData(responseData);
+ }
+ }
+
+ private static class FakeTaskPlugin extends SonarPlugin {
+
+ @Override
+ public List getExtensions() {
+ return Arrays.asList(FakeTask.DEF, FakeTask.class, BrokenTask.DEF, BrokenTask.class);
+ }
+
+ }
+
+ private static class FakeTask implements Task {
+
+ public static final TaskDefinition DEF = TaskDefinition.builder().key("fake").description("Fake description").taskClass(FakeTask.class).build();
+
+ @Override
+ public void execute() {
+ // TODO Auto-generated method stub
+
+ }
+
+ }
+
+ private static class BrokenTask implements Task {
+
+ public static final TaskDefinition DEF = TaskDefinition.builder().key("broken").description("Broken description").taskClass(BrokenTask.class).build();
+ private final Actions serverSideComponent;
+
+ public BrokenTask(Actions serverSideComponent) {
+ this.serverSideComponent = serverSideComponent;
+ }
+
+ @Override
+ public void execute() {
+ System.out.println(serverSideComponent.list());
+ ;
+
+ }
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/CoveragePerTestMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/CoveragePerTestMediumTest.java
new file mode 100644
index 00000000000..d62d978ecaf
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/CoveragePerTestMediumTest.java
@@ -0,0 +1,160 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.tests;
+
+import org.hamcrest.Description;
+
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CoveragePerTestMediumTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ // SONAR-6183
+ public void invalidCoverage() throws IOException {
+ File baseDir = createTestFiles();
+ File srcDir = new File(baseDir, "src");
+
+ File coverageFile = new File(srcDir, "sample.xoo.coverage");
+ FileUtils.write(coverageFile, "0:2\n");
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Error processing line 1 of file");
+ exception.expectCause(new TypeSafeMatcher<Throwable>() {
+
+ @Override
+ public void describeTo(Description description) {
+ // nothing to do
+ }
+
+ @Override
+ protected boolean matchesSafely(Throwable item) {
+ return item.getMessage().contains("Line number must be strictly positive");
+ }
+ });
+ runTask(baseDir);
+
+ }
+
+ @Test
+ public void coveragePerTestInReport() throws IOException {
+ File baseDir = createTestFiles();
+ File testDir = new File(baseDir, "test");
+
+ File xooTestExecutionFile = new File(testDir, "sampleTest.xoo.test");
+ FileUtils.write(xooTestExecutionFile, "some test:4:::OK:UNIT\n" +
+ "another test:10:::OK:UNIT\n" +
+ "test without coverage:10:::OK:UNIT\n");
+
+ File xooCoveragePerTestFile = new File(testDir, "sampleTest.xoo.testcoverage");
+ FileUtils.write(xooCoveragePerTestFile, "some test;src/sample.xoo,10,11;src/sample2.xoo,1,2\n" +
+ "another test;src/sample.xoo,10,20\n");
+
+ TaskResult result = runTask(baseDir);
+
+ InputFile file = result.inputFile("test/sampleTest.xoo");
+ org.sonar.scanner.protocol.output.ScannerReport.CoverageDetail someTest = result.coveragePerTestFor(file, "some test");
+ assertThat(someTest.getCoveredFileList()).hasSize(2);
+ assertThat(someTest.getCoveredFile(0).getFileRef()).isGreaterThan(0);
+ assertThat(someTest.getCoveredFile(0).getCoveredLineList()).containsExactly(10, 11);
+ assertThat(someTest.getCoveredFile(1).getFileRef()).isGreaterThan(0);
+ assertThat(someTest.getCoveredFile(1).getCoveredLineList()).containsExactly(1, 2);
+
+ org.sonar.scanner.protocol.output.ScannerReport.CoverageDetail anotherTest = result.coveragePerTestFor(file, "another test");
+ assertThat(anotherTest.getCoveredFileList()).hasSize(1);
+ assertThat(anotherTest.getCoveredFile(0).getFileRef()).isGreaterThan(0);
+ assertThat(anotherTest.getCoveredFile(0).getCoveredLineList()).containsExactly(10, 20);
+ }
+
+ private TaskResult runTask(File baseDir) {
+ return tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.tests", "test")
+ .build())
+ .start();
+ }
+
+ private File createTestFiles() throws IOException {
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+ File testDir = new File(baseDir, "test");
+ testDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "foo");
+
+ File xooFile2 = new File(srcDir, "sample2.xoo");
+ FileUtils.write(xooFile2, "foo");
+
+ File xooTestFile = new File(testDir, "sampleTest.xoo");
+ FileUtils.write(xooTestFile, "failure\nerror\nok\nskipped");
+
+ File xooTestFile2 = new File(testDir, "sample2Test.xoo");
+ FileUtils.write(xooTestFile2, "test file tests");
+
+ return baseDir;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/TestExecutionMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/TestExecutionMediumTest.java
new file mode 100644
index 00000000000..cae559f16c5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/mediumtest/tests/TestExecutionMediumTest.java
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.mediumtest.tests;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.scanner.protocol.Constants.TestStatus;
+import org.sonar.xoo.XooPlugin;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestExecutionMediumTest {
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ @Before
+ public void prepare() {
+ tester.start();
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void unitTests() throws IOException {
+
+ File baseDir = temp.getRoot();
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+ File testDir = new File(baseDir, "test");
+ testDir.mkdir();
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, "foo");
+
+ File xooTestFile = new File(testDir, "sampleTest.xoo");
+ FileUtils.write(xooTestFile, "failure\nerror\nok\nskipped");
+
+ File xooTestExecutionFile = new File(testDir, "sampleTest.xoo.test");
+ FileUtils.write(xooTestExecutionFile, "skipped::::SKIPPED:UNIT\n" +
+ "failure:2:Failure::FAILURE:UNIT\n" +
+ "error:2:Error:The stack:ERROR:UNIT\n" +
+ "success:4:::OK:INTEGRATION");
+
+ TaskResult result = tester.newTask()
+ .properties(ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project")
+ .put("sonar.sources", "src")
+ .put("sonar.tests", "test")
+ .build())
+ .start();
+
+ InputFile file = result.inputFile("test/sampleTest.xoo");
+ org.sonar.scanner.protocol.output.ScannerReport.Test success = result.testExecutionFor(file, "success");
+ assertThat(success.getDurationInMs()).isEqualTo(4);
+ assertThat(success.getStatus()).isEqualTo(TestStatus.OK);
+
+ org.sonar.scanner.protocol.output.ScannerReport.Test error = result.testExecutionFor(file, "error");
+ assertThat(error.getDurationInMs()).isEqualTo(2);
+ assertThat(error.getStatus()).isEqualTo(TestStatus.ERROR);
+ assertThat(error.getMsg()).isEqualTo("Error");
+ assertThat(error.getStacktrace()).isEqualTo("The stack");
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/phases/PostJobsExecutorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/phases/PostJobsExecutorTest.java
new file mode 100644
index 00000000000..f9fda97b21c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/phases/PostJobsExecutorTest.java
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.phases;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.batch.PostJob;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.bootstrap.BatchExtensionDictionnary;
+import org.sonar.batch.events.EventBus;
+
+import java.util.Arrays;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PostJobsExecutorTest {
+ PostJobsExecutor executor;
+
+ Project project = new Project("project");
+ BatchExtensionDictionnary selector = mock(BatchExtensionDictionnary.class);
+ PostJob job1 = mock(PostJob.class);
+ PostJob job2 = mock(PostJob.class);
+ SensorContext context = mock(SensorContext.class);
+
+ @Before
+ public void setUp() {
+ executor = new PostJobsExecutor(selector, project, mock(EventBus.class));
+ }
+
+ @Test
+ public void should_execute_post_jobs() {
+ when(selector.select(PostJob.class, project, true, null)).thenReturn(Arrays.asList(job1, job2));
+
+ executor.execute(context);
+
+ verify(job1).executeOn(project, context);
+ verify(job2).executeOn(project, context);
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/platform/DefaultServerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/platform/DefaultServerTest.java
new file mode 100644
index 00000000000..801848354fc
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/platform/DefaultServerTest.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.platform;
+
+import org.sonar.batch.bootstrap.GlobalProperties;
+
+import org.junit.Test;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultServerTest {
+
+ @Test
+ public void shouldLoadServerProperties() {
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.SERVER_ID, "123");
+ settings.setProperty(CoreProperties.SERVER_VERSION, "2.2");
+ settings.setProperty(CoreProperties.SERVER_STARTTIME, "2010-05-18T17:59:00+0000");
+ settings.setProperty(CoreProperties.PERMANENT_SERVER_ID, "abcde");
+ GlobalProperties props = mock(GlobalProperties.class);
+ when(props.property("sonar.host.url")).thenReturn("http://foo.com");
+
+ DefaultServer metadata = new DefaultServer(settings, props);
+
+ assertThat(metadata.getId()).isEqualTo("123");
+ assertThat(metadata.getVersion()).isEqualTo("2.2");
+ assertThat(metadata.getStartedAt()).isNotNull();
+ assertThat(metadata.getURL()).isEqualTo("http://foo.com");
+ assertThat(metadata.getPermanentServerId()).isEqualTo("abcde");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/DefaultPostJobContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/DefaultPostJobContextTest.java
new file mode 100644
index 00000000000..12f00a55d90
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/DefaultPostJobContextTest.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.postjob;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.postjob.issue.Issue;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.File;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.issue.IssueCache;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultPostJobContextTest {
+
+ private IssueCache issueCache;
+ private BatchComponentCache resourceCache;
+ private AnalysisMode analysisMode;
+ private DefaultPostJobContext context;
+ private Settings settings;
+
+ @Before
+ public void prepare() {
+ issueCache = mock(IssueCache.class);
+ resourceCache = new BatchComponentCache();
+ analysisMode = mock(AnalysisMode.class);
+ settings = new Settings();
+ context = new DefaultPostJobContext(settings, analysisMode, issueCache, resourceCache);
+ }
+
+ @Test
+ public void test() {
+ assertThat(context.settings()).isSameAs(settings);
+ assertThat(context.analysisMode()).isSameAs(analysisMode);
+
+ TrackedIssue defaultIssue = new TrackedIssue();
+ defaultIssue.setComponentKey("foo:src/Foo.php");
+ defaultIssue.setGap(2.0);
+ defaultIssue.setNew(true);
+ defaultIssue.setKey("xyz");
+ defaultIssue.setStartLine(1);
+ defaultIssue.setMessage("msg");
+ defaultIssue.setSeverity("BLOCKER");
+ when(issueCache.all()).thenReturn(Arrays.asList(defaultIssue));
+
+ Issue issue = context.issues().iterator().next();
+ assertThat(issue.componentKey()).isEqualTo("foo:src/Foo.php");
+ assertThat(issue.effortToFix()).isEqualTo(2.0);
+ assertThat(issue.isNew()).isTrue();
+ assertThat(issue.key()).isEqualTo("xyz");
+ assertThat(issue.line()).isEqualTo(1);
+ assertThat(issue.message()).isEqualTo("msg");
+ assertThat(issue.severity()).isEqualTo(Severity.BLOCKER);
+ assertThat(issue.inputComponent()).isNull();
+
+ InputFile inputPath = mock(InputFile.class);
+ resourceCache.add(File.create("src/Foo.php").setEffectiveKey("foo:src/Foo.php"), null).setInputComponent(inputPath);
+ assertThat(issue.inputComponent()).isEqualTo(inputPath);
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/PostJobOptimizerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/PostJobOptimizerTest.java
new file mode 100644
index 00000000000..e5d30f1dfa3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/postjob/PostJobOptimizerTest.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.postjob;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.postjob.internal.DefaultPostJobDescriptor;
+import org.sonar.api.config.Settings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class PostJobOptimizerTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private PostJobOptimizer optimizer;
+ private Settings settings;
+ private AnalysisMode analysisMode;
+
+ @Before
+ public void prepare() {
+ settings = new Settings();
+ analysisMode = mock(AnalysisMode.class);
+ optimizer = new PostJobOptimizer(settings, analysisMode);
+ }
+
+ @Test
+ public void should_run_analyzer_with_no_metadata() {
+ DefaultPostJobDescriptor descriptor = new DefaultPostJobDescriptor();
+
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+ @Test
+ public void should_optimize_on_settings() {
+ DefaultPostJobDescriptor descriptor = new DefaultPostJobDescriptor()
+ .requireProperty("sonar.foo.reportPath");
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ settings.setProperty("sonar.foo.reportPath", "foo");
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+ @Test
+ public void should_disabled_in_issues_mode() {
+ DefaultPostJobDescriptor descriptor = new DefaultPostJobDescriptor()
+ .disabledInIssues();
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+
+ when(analysisMode.isIssues()).thenReturn(true);
+
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/profiling/PhasesSumUpTimeProfilerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/profiling/PhasesSumUpTimeProfilerTest.java
new file mode 100644
index 00000000000..9c8e4ed8d56
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/profiling/PhasesSumUpTimeProfilerTest.java
@@ -0,0 +1,410 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.profiling;
+
+import com.google.common.collect.Maps;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.Initializer;
+import org.sonar.api.batch.PostJob;
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.batch.events.DecoratorExecutionHandler;
+import org.sonar.api.batch.events.DecoratorsPhaseHandler;
+import org.sonar.api.batch.events.InitializerExecutionHandler;
+import org.sonar.api.batch.events.InitializersPhaseHandler;
+import org.sonar.api.batch.events.PostJobExecutionHandler;
+import org.sonar.api.batch.events.PostJobsPhaseHandler;
+import org.sonar.api.batch.events.ProjectAnalysisHandler;
+import org.sonar.api.batch.events.ProjectAnalysisHandler.ProjectAnalysisEvent;
+import org.sonar.api.batch.events.SensorExecutionHandler;
+import org.sonar.api.batch.events.SensorExecutionHandler.SensorExecutionEvent;
+import org.sonar.api.batch.events.SensorsPhaseHandler;
+import org.sonar.api.batch.events.SensorsPhaseHandler.SensorsPhaseEvent;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.System2;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.events.BatchStepEvent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+public class PhasesSumUpTimeProfilerTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private MockedSystem clock;
+ private PhasesSumUpTimeProfiler profiler;
+
+ @Before
+ public void prepare() throws Exception {
+ clock = new MockedSystem();
+ Map<String, String> props = Maps.newHashMap();
+ props.put(CoreProperties.WORKING_DIRECTORY, temp.newFolder().getAbsolutePath());
+ profiler = new PhasesSumUpTimeProfiler(clock, new GlobalProperties(props));
+ }
+
+ @Test
+ public void testSimpleProject() throws InterruptedException {
+
+ final Project project = mockProject("my:project", true);
+ when(project.getModules()).thenReturn(Collections.<Project>emptyList());
+
+ fakeAnalysis(profiler, project);
+
+ assertThat(profiler.currentModuleProfiling.getProfilingPerPhase(Phase.INIT).getProfilingPerItem(new FakeInitializer()).totalTime()).isEqualTo(7L);
+ assertThat(profiler.currentModuleProfiling.getProfilingPerPhase(Phase.SENSOR).getProfilingPerItem(new FakeSensor()).totalTime()).isEqualTo(10L);
+ assertThat(profiler.currentModuleProfiling.getProfilingPerPhase(Phase.POSTJOB).getProfilingPerItem(new FakePostJob()).totalTime()).isEqualTo(30L);
+ assertThat(profiler.currentModuleProfiling.getProfilingPerBatchStep("Free memory").totalTime()).isEqualTo(9L);
+ }
+
+ @Test
+ public void testMultimoduleProject() throws InterruptedException {
+ final Project project = mockProject("project root", true);
+ final Project moduleA = mockProject("moduleA", false);
+ final Project moduleB = mockProject("moduleB", false);
+ when(project.getModules()).thenReturn(Arrays.asList(moduleA, moduleB));
+
+ fakeAnalysis(profiler, moduleA);
+ fakeAnalysis(profiler, moduleB);
+ fakeAnalysis(profiler, project);
+
+ assertThat(profiler.currentModuleProfiling.getProfilingPerPhase(Phase.INIT).getProfilingPerItem(new FakeInitializer()).totalTime()).isEqualTo(7L);
+ assertThat(profiler.currentModuleProfiling.getProfilingPerPhase(Phase.SENSOR).getProfilingPerItem(new FakeSensor()).totalTime()).isEqualTo(10L);
+ assertThat(profiler.currentModuleProfiling.getProfilingPerPhase(Phase.POSTJOB).getProfilingPerItem(new FakePostJob()).totalTime()).isEqualTo(30L);
+
+ assertThat(profiler.totalProfiling.getProfilingPerPhase(Phase.INIT).getProfilingPerItem(new FakeInitializer()).totalTime()).isEqualTo(21L);
+ assertThat(profiler.totalProfiling.getProfilingPerPhase(Phase.SENSOR).getProfilingPerItem(new FakeSensor()).totalTime()).isEqualTo(30L);
+ assertThat(profiler.totalProfiling.getProfilingPerPhase(Phase.POSTJOB).getProfilingPerItem(new FakePostJob()).totalTime()).isEqualTo(90L);
+ }
+
+ @Test
+ public void testDisplayTimings() {
+ AbstractTimeProfiling profiling = new AbstractTimeProfiling(System2.INSTANCE) {
+ };
+
+ profiling.setTotalTime(5);
+ assertThat(profiling.totalTimeAsString()).isEqualTo("5ms");
+
+ profiling.setTotalTime(5 * 1000 + 12);
+ assertThat(profiling.totalTimeAsString()).isEqualTo("5s");
+
+ profiling.setTotalTime(5 * 60 * 1000 + 12 * 1000);
+ assertThat(profiling.totalTimeAsString()).isEqualTo("5min 12s");
+
+ profiling.setTotalTime(5 * 60 * 1000);
+ assertThat(profiling.totalTimeAsString()).isEqualTo("5min");
+ }
+
+ private class MockedSystem extends System2 {
+ private long now = 0;
+
+ @Override
+ public long now() {
+ return now;
+ }
+
+ public void sleep(long duration) {
+ now += duration;
+ }
+ }
+
+ private Project mockProject(String name, boolean isRoot) {
+ final Project project = spy(new Project("myProject"));
+ when(project.isRoot()).thenReturn(isRoot);
+ when(project.getName()).thenReturn(name);
+ return project;
+ }
+
+ private void fakeAnalysis(PhasesSumUpTimeProfiler profiler, final Project module) {
+ // Start of moduleA
+ profiler.onProjectAnalysis(projectEvent(module, true));
+ initializerPhase(profiler);
+ sensorPhase(profiler);
+ postJobPhase(profiler);
+ batchStep(profiler);
+ // End of moduleA
+ profiler.onProjectAnalysis(projectEvent(module, false));
+ }
+
+ private void batchStep(PhasesSumUpTimeProfiler profiler) {
+ // Start of batch step
+ profiler.onBatchStep(new BatchStepEvent("Free memory", true));
+ clock.sleep(9);
+ // End of batch step
+ profiler.onBatchStep(new BatchStepEvent("Free memory", false));
+ }
+
+ private void initializerPhase(PhasesSumUpTimeProfiler profiler) {
+ Initializer initializer = new FakeInitializer();
+ // Start of initializer phase
+ profiler.onInitializersPhase(initializersEvent(true));
+ // Start of an initializer
+ profiler.onInitializerExecution(initializerEvent(initializer, true));
+ clock.sleep(7);
+ // End of an initializer
+ profiler.onInitializerExecution(initializerEvent(initializer, false));
+ // End of initializer phase
+ profiler.onInitializersPhase(initializersEvent(false));
+ }
+
+ private void sensorPhase(PhasesSumUpTimeProfiler profiler) {
+ Sensor sensor = new FakeSensor();
+ // Start of sensor phase
+ profiler.onSensorsPhase(sensorsEvent(true));
+ // Start of a Sensor
+ profiler.onSensorExecution(sensorEvent(sensor, true));
+ clock.sleep(10);
+ // End of a Sensor
+ profiler.onSensorExecution(sensorEvent(sensor, false));
+ // End of sensor phase
+ profiler.onSensorsPhase(sensorsEvent(false));
+ }
+
+ private void postJobPhase(PhasesSumUpTimeProfiler profiler) {
+ PostJob postJob = new FakePostJob();
+ // Start of sensor phase
+ profiler.onPostJobsPhase(postJobsEvent(true));
+ // Start of a Sensor
+ profiler.onPostJobExecution(postJobEvent(postJob, true));
+ clock.sleep(30);
+ // End of a Sensor
+ profiler.onPostJobExecution(postJobEvent(postJob, false));
+ // End of sensor phase
+ profiler.onPostJobsPhase(postJobsEvent(false));
+ }
+
+ private SensorExecutionEvent sensorEvent(final Sensor sensor, final boolean start) {
+ return new SensorExecutionHandler.SensorExecutionEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public Sensor getSensor() {
+ return sensor;
+ }
+ };
+ }
+
+ private InitializerExecutionHandler.InitializerExecutionEvent initializerEvent(final Initializer initializer, final boolean start) {
+ return new InitializerExecutionHandler.InitializerExecutionEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public Initializer getInitializer() {
+ return initializer;
+ }
+ };
+ }
+
+ private DecoratorExecutionHandler.DecoratorExecutionEvent decoratorEvent(final Decorator decorator, final boolean start) {
+ return new DecoratorExecutionHandler.DecoratorExecutionEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public Decorator getDecorator() {
+ return decorator;
+ }
+ };
+ }
+
+ private PostJobExecutionHandler.PostJobExecutionEvent postJobEvent(final PostJob postJob, final boolean start) {
+ return new PostJobExecutionHandler.PostJobExecutionEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public PostJob getPostJob() {
+ return postJob;
+ }
+ };
+ }
+
+ private SensorsPhaseEvent sensorsEvent(final boolean start) {
+ return new SensorsPhaseHandler.SensorsPhaseEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public List<Sensor> getSensors() {
+ return null;
+ }
+ };
+ }
+
+ private InitializersPhaseHandler.InitializersPhaseEvent initializersEvent(final boolean start) {
+ return new InitializersPhaseHandler.InitializersPhaseEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public List<Initializer> getInitializers() {
+ return null;
+ }
+ };
+ }
+
+ private PostJobsPhaseHandler.PostJobsPhaseEvent postJobsEvent(final boolean start) {
+ return new PostJobsPhaseHandler.PostJobsPhaseEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public List<PostJob> getPostJobs() {
+ return null;
+ }
+ };
+ }
+
+ private DecoratorsPhaseHandler.DecoratorsPhaseEvent decoratorsEvent(final boolean start) {
+ return new DecoratorsPhaseHandler.DecoratorsPhaseEvent() {
+
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public List<Decorator> getDecorators() {
+ return null;
+ }
+ };
+ }
+
+ private ProjectAnalysisEvent projectEvent(final Project project, final boolean start) {
+ return new ProjectAnalysisHandler.ProjectAnalysisEvent() {
+ @Override
+ public boolean isStart() {
+ return start;
+ }
+
+ @Override
+ public boolean isEnd() {
+ return !start;
+ }
+
+ @Override
+ public Project getProject() {
+ return project;
+ }
+ };
+ }
+
+ public class FakeSensor implements Sensor {
+ @Override
+ public void analyse(Project project, SensorContext context) {
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+ }
+
+ public class FakeInitializer extends Initializer {
+ @Override
+ public void execute(Project project) {
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+ }
+
+ public class FakePostJob implements PostJob {
+ @Override
+ public void executeOn(Project project, SensorContext context) {
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ActiveRulesPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ActiveRulesPublisherTest.java
new file mode 100644
index 00000000000..4b912b22d7b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ActiveRulesPublisherTest.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.File;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.batch.rule.internal.DefaultActiveRules;
+import org.sonar.api.batch.rule.internal.NewActiveRule;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.scanner.protocol.Constants;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ActiveRulesPublisherTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void write() throws Exception {
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+
+ NewActiveRule ar = new ActiveRulesBuilder().create(RuleKey.of("java", "S001")).setSeverity("BLOCKER").setParam("p1", "v1");
+ ActiveRules activeRules = new DefaultActiveRules(Arrays.asList(ar));
+
+ ActiveRulesPublisher underTest = new ActiveRulesPublisher(activeRules);
+ underTest.publish(writer);
+
+ ScannerReportReader reader = new ScannerReportReader(outputDir);
+ try (CloseableIterator<ScannerReport.ActiveRule> readIt = reader.readActiveRules()) {
+ ScannerReport.ActiveRule reportAr = readIt.next();
+ assertThat(reportAr.getRuleRepository()).isEqualTo("java");
+ assertThat(reportAr.getRuleKey()).isEqualTo("S001");
+ assertThat(reportAr.getSeverity()).isEqualTo(Constants.Severity.BLOCKER);
+ assertThat(reportAr.getParamCount()).isEqualTo(1);
+ assertThat(reportAr.getParam(0).getKey()).isEqualTo("p1");
+ assertThat(reportAr.getParam(0).getValue()).isEqualTo("v1");
+
+ assertThat(readIt.hasNext()).isFalse();
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/AnalysisContextReportPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/AnalysisContextReportPublisherTest.java
new file mode 100644
index 00000000000..320941119b3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/AnalysisContextReportPublisherTest.java
@@ -0,0 +1,207 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+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.batch.AnalysisMode;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.batch.bootstrap.BatchPluginRepository;
+import org.sonar.batch.repository.ProjectRepositories;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+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.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class AnalysisContextReportPublisherTest {
+
+ private static final String BIZ = "BIZ";
+ private static final String FOO = "FOO";
+ private static final String SONAR_SKIP = "sonar.skip";
+ private static final String COM_FOO = "com.foo";
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private BatchPluginRepository pluginRepo = mock(BatchPluginRepository.class);
+ private AnalysisContextReportPublisher publisher;
+ private AnalysisMode analysisMode = mock(AnalysisMode.class);
+ private System2 system2;
+ private ProjectRepositories projectRepos;
+
+ @Before
+ public void prepare() throws Exception {
+ logTester.setLevel(LoggerLevel.INFO);
+ system2 = mock(System2.class);
+ when(system2.properties()).thenReturn(new Properties());
+ projectRepos = mock(ProjectRepositories.class);
+ publisher = new AnalysisContextReportPublisher(analysisMode, pluginRepo, system2, projectRepos);
+ }
+
+ @Test
+ public void shouldOnlyDumpPluginsByDefault() throws Exception {
+ when(pluginRepo.getPluginInfos()).thenReturn(Arrays.asList(new PluginInfo("xoo").setName("Xoo").setVersion(Version.create("1.0"))));
+
+ ScannerReportWriter writer = new ScannerReportWriter(temp.newFolder());
+ publisher.init(writer);
+
+ assertThat(writer.getFileStructure().analysisLog()).exists();
+ assertThat(FileUtils.readFileToString(writer.getFileStructure().analysisLog())).contains("Xoo 1.0 (xoo)");
+
+ verifyZeroInteractions(system2);
+ }
+
+ @Test
+ public void shouldNotDumpInIssuesMode() throws Exception {
+ when(analysisMode.isIssues()).thenReturn(true);
+
+ ScannerReportWriter writer = new ScannerReportWriter(temp.newFolder());
+ publisher.init(writer);
+ publisher.dumpSettings(ProjectDefinition.create().setProperty("sonar.projectKey", "foo"));
+
+ assertThat(writer.getFileStructure().analysisLog()).doesNotExist();
+ }
+
+ @Test
+ public void dumpServerSideProps() throws Exception {
+ logTester.setLevel(LoggerLevel.DEBUG);
+ ScannerReportWriter writer = new ScannerReportWriter(temp.newFolder());
+ publisher.init(writer);
+
+ when(projectRepos.moduleExists("foo")).thenReturn(true);
+ when(projectRepos.settings("foo")).thenReturn(ImmutableMap.of(COM_FOO, "bar", SONAR_SKIP, "true"));
+
+ publisher.dumpSettings(ProjectDefinition.create()
+ .setProperty("sonar.projectKey", "foo"));
+
+ String content = FileUtils.readFileToString(writer.getFileStructure().analysisLog());
+ assertThat(content).doesNotContain(COM_FOO);
+ assertThat(content).containsOnlyOnce(SONAR_SKIP);
+ }
+
+ @Test
+ public void shouldNotDumpSQPropsInSystemProps() throws Exception {
+ logTester.setLevel(LoggerLevel.DEBUG);
+ ScannerReportWriter writer = new ScannerReportWriter(temp.newFolder());
+ Properties props = new Properties();
+ props.setProperty(COM_FOO, "bar");
+ props.setProperty(SONAR_SKIP, "true");
+ when(system2.properties()).thenReturn(props);
+ publisher.init(writer);
+
+ String content = FileUtils.readFileToString(writer.getFileStructure().analysisLog());
+ assertThat(content).containsOnlyOnce(COM_FOO);
+ assertThat(content).doesNotContain(SONAR_SKIP);
+
+ publisher.dumpSettings(ProjectDefinition.create()
+ .setProperty("sonar.projectKey", "foo")
+ .setProperty(COM_FOO, "bar")
+ .setProperty(SONAR_SKIP, "true"));
+
+ content = FileUtils.readFileToString(writer.getFileStructure().analysisLog());
+ assertThat(content).containsOnlyOnce(COM_FOO);
+ assertThat(content).containsOnlyOnce(SONAR_SKIP);
+ }
+
+ @Test
+ public void shouldNotDumpEnvTwice() throws Exception {
+ logTester.setLevel(LoggerLevel.DEBUG);
+ ScannerReportWriter writer = new ScannerReportWriter(temp.newFolder());
+
+ Map<String, String> env = new HashMap<>();
+ env.put(FOO, "BAR");
+ env.put(BIZ, "BAZ");
+ when(system2.envVariables()).thenReturn(env);
+ publisher.init(writer);
+
+ String content = FileUtils.readFileToString(writer.getFileStructure().analysisLog());
+ assertThat(content).containsOnlyOnce(FOO);
+ assertThat(content).containsOnlyOnce(BIZ);
+ assertThat(content).containsSequence(BIZ, FOO);
+
+ publisher.dumpSettings(ProjectDefinition.create()
+ .setProperty("sonar.projectKey", "foo")
+ .setProperty("env." + FOO, "BAR"));
+
+ content = FileUtils.readFileToString(writer.getFileStructure().analysisLog());
+ assertThat(content).containsOnlyOnce(FOO);
+ assertThat(content).containsOnlyOnce(BIZ);
+ assertThat(content).doesNotContain("env." + FOO);
+ }
+
+ @Test
+ public void shouldNotDumpSensitiveProperties() throws Exception {
+ ScannerReportWriter writer = new ScannerReportWriter(temp.newFolder());
+ publisher.init(writer);
+
+ assertThat(writer.getFileStructure().analysisLog()).exists();
+
+ publisher.dumpSettings(ProjectDefinition.create()
+ .setProperty("sonar.projectKey", "foo")
+ .setProperty("sonar.projectKey", "foo")
+ .setProperty("sonar.password", "azerty")
+ .setProperty("sonar.cpp.license.secured", "AZERTY"));
+
+ assertThat(FileUtils.readFileToString(writer.getFileStructure().analysisLog())).containsSequence(
+ "sonar.cpp.license.secured=******",
+ "sonar.password=******",
+ "sonar.projectKey=foo");
+ }
+
+ // SONAR-7371
+ @Test
+ public void dontDumpParentProps() throws Exception {
+ logTester.setLevel(LoggerLevel.DEBUG);
+ ScannerReportWriter writer = new ScannerReportWriter(temp.newFolder());
+ publisher.init(writer);
+
+ ProjectDefinition module = ProjectDefinition.create()
+ .setProperty("sonar.projectKey", "foo")
+ .setProperty(SONAR_SKIP, "true");
+
+ ProjectDefinition.create()
+ .setProperty("sonar.projectKey", "parent")
+ .setProperty(SONAR_SKIP, "true")
+ .addSubProject(module);
+
+ publisher.dumpSettings(module);
+
+ String content = FileUtils.readFileToString(writer.getFileStructure().analysisLog());
+ assertThat(content).doesNotContain(SONAR_SKIP);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ComponentsPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ComponentsPublisherTest.java
new file mode 100644
index 00000000000..0a4ed8d69cd
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ComponentsPublisherTest.java
@@ -0,0 +1,170 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.File;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.internal.DefaultInputDir;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.Java;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+import org.sonar.scanner.protocol.Constants.ComponentLinkType;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.protocol.output.FileStructure;
+import org.sonar.scanner.protocol.output.ScannerReport.Component;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ComponentsPublisherTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ BatchComponentCache resourceCache = new BatchComponentCache();
+
+ @Test
+ public void add_components_to_report() throws Exception {
+
+ ProjectDefinition rootDef = ProjectDefinition.create().setKey("foo");
+ rootDef.properties().put(CoreProperties.PROJECT_VERSION_PROPERTY, "1.0");
+ Project root = new Project("foo").setName("Root project").setDescription("Root description")
+ .setAnalysisDate(DateUtils.parseDate(("2012-12-12")));
+ root.setId(1).setUuid("PROJECT_UUID");
+ resourceCache.add(root, null).setInputComponent(new DefaultInputModule("foo"));
+
+ Project module1 = new Project("module1").setName("Module1").setDescription("Module description");
+ module1.setParent(root);
+ module1.setId(2).setUuid("MODULE_UUID");
+ resourceCache.add(module1, root).setInputComponent(new DefaultInputModule("module1"));
+ rootDef.addSubProject(ProjectDefinition.create().setKey("module1"));
+
+ Directory dir = Directory.create("src");
+ dir.setEffectiveKey("module1:src");
+ dir.setId(3).setUuid("DIR_UUID");
+ resourceCache.add(dir, module1).setInputComponent(new DefaultInputDir("foo", "src"));
+
+ org.sonar.api.resources.File file = org.sonar.api.resources.File.create("src/Foo.java", Java.INSTANCE, false);
+ file.setEffectiveKey("module1:src/Foo.java");
+ file.setId(4).setUuid("FILE_UUID");
+ resourceCache.add(file, dir).setInputComponent(new DefaultInputFile("module1", "src/Foo.java").setLines(2));
+
+ org.sonar.api.resources.File fileWithoutLang = org.sonar.api.resources.File.create("src/make", null, false);
+ fileWithoutLang.setEffectiveKey("module1:src/make");
+ fileWithoutLang.setId(5).setUuid("FILE_WITHOUT_LANG_UUID");
+ resourceCache.add(fileWithoutLang, dir).setInputComponent(new DefaultInputFile("module1", "src/make").setLines(10));
+
+ org.sonar.api.resources.File testFile = org.sonar.api.resources.File.create("test/FooTest.java", Java.INSTANCE, true);
+ testFile.setEffectiveKey("module1:test/FooTest.java");
+ testFile.setId(6).setUuid("TEST_FILE_UUID");
+ resourceCache.add(testFile, dir).setInputComponent(new DefaultInputFile("module1", "test/FooTest.java").setLines(4));
+
+ ImmutableProjectReactor reactor = new ImmutableProjectReactor(rootDef);
+
+ ComponentsPublisher publisher = new ComponentsPublisher(reactor, resourceCache);
+
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+ publisher.publish(writer);
+
+ assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 1)).isTrue();
+ assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 2)).isTrue();
+ assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 3)).isTrue();
+ assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 4)).isTrue();
+ assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 5)).isTrue();
+ assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 6)).isTrue();
+
+ // no such reference
+ assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 7)).isFalse();
+
+ ScannerReportReader reader = new ScannerReportReader(outputDir);
+ Component rootProtobuf = reader.readComponent(1);
+ assertThat(rootProtobuf.getKey()).isEqualTo("foo");
+ assertThat(rootProtobuf.getDescription()).isEqualTo("Root description");
+ assertThat(rootProtobuf.getVersion()).isEqualTo("1.0");
+ assertThat(rootProtobuf.getLinkCount()).isEqualTo(0);
+
+ Component module1Protobuf = reader.readComponent(2);
+ assertThat(module1Protobuf.getKey()).isEqualTo("module1");
+ assertThat(module1Protobuf.getDescription()).isEqualTo("Module description");
+ assertThat(module1Protobuf.getVersion()).isEqualTo("1.0");
+ }
+
+ @Test
+ public void add_components_with_links_and_branch() throws Exception {
+ // inputs
+ ProjectDefinition rootDef = ProjectDefinition.create().setKey("foo");
+ rootDef.properties().put(CoreProperties.PROJECT_VERSION_PROPERTY, "1.0");
+ Project root = new Project("foo:my_branch").setName("Root project")
+ .setAnalysisDate(DateUtils.parseDate(("2012-12-12")));
+ root.setId(1).setUuid("PROJECT_UUID");
+ resourceCache.add(root, null).setInputComponent(new DefaultInputModule("foo"));
+ rootDef.properties().put(CoreProperties.LINKS_HOME_PAGE, "http://home");
+ rootDef.properties().put(CoreProperties.PROJECT_BRANCH_PROPERTY, "my_branch");
+
+ Project module1 = new Project("module1:my_branch").setName("Module1");
+ module1.setParent(root);
+ module1.setId(2).setUuid("MODULE_UUID");
+ resourceCache.add(module1, root).setInputComponent(new DefaultInputModule("module1"));
+ ProjectDefinition moduleDef = ProjectDefinition.create().setKey("module1");
+ moduleDef.properties().put(CoreProperties.LINKS_CI, "http://ci");
+ rootDef.addSubProject(moduleDef);
+
+ Directory dir = Directory.create("src");
+ dir.setEffectiveKey("module1:my_branch:my_branch:src");
+ dir.setId(3).setUuid("DIR_UUID");
+ resourceCache.add(dir, module1).setInputComponent(new DefaultInputDir("foo", "src"));
+
+ org.sonar.api.resources.File file = org.sonar.api.resources.File.create("src/Foo.java", Java.INSTANCE, false);
+ file.setEffectiveKey("module1:my_branch:my_branch:src/Foo.java");
+ file.setId(4).setUuid("FILE_UUID");
+ resourceCache.add(file, dir).setInputComponent(new DefaultInputFile("module1", "src/Foo.java").setLines(2));
+
+ ImmutableProjectReactor reactor = new ImmutableProjectReactor(rootDef);
+
+ ComponentsPublisher publisher = new ComponentsPublisher(reactor, resourceCache);
+
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+ publisher.publish(writer);
+
+ ScannerReportReader reader = new ScannerReportReader(outputDir);
+ Component rootProtobuf = reader.readComponent(1);
+ assertThat(rootProtobuf.getVersion()).isEqualTo("1.0");
+ assertThat(rootProtobuf.getLinkCount()).isEqualTo(1);
+ assertThat(rootProtobuf.getLink(0).getType()).isEqualTo(ComponentLinkType.HOME);
+ assertThat(rootProtobuf.getLink(0).getHref()).isEqualTo("http://home");
+
+ Component module1Protobuf = reader.readComponent(2);
+ assertThat(module1Protobuf.getVersion()).isEqualTo("1.0");
+ assertThat(module1Protobuf.getLinkCount()).isEqualTo(1);
+ assertThat(module1Protobuf.getLink(0).getType()).isEqualTo(ComponentLinkType.CI);
+ assertThat(module1Protobuf.getLink(0).getHref()).isEqualTo("http://ci");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/CoveragePublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/CoveragePublisherTest.java
new file mode 100644
index 00000000000..72c31cf4928
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/CoveragePublisherTest.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.File;
+import java.util.Date;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.protocol.output.ScannerReport.Coverage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class CoveragePublisherTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private MeasureCache measureCache;
+ private CoveragePublisher publisher;
+
+ private org.sonar.api.resources.Resource sampleFile;
+
+ @Before
+ public void prepare() {
+ Project p = new Project("foo").setAnalysisDate(new Date(1234567L));
+ BatchComponentCache resourceCache = new BatchComponentCache();
+ sampleFile = org.sonar.api.resources.File.create("src/Foo.php").setEffectiveKey("foo:src/Foo.php");
+ resourceCache.add(p, null).setInputComponent(new DefaultInputModule("foo"));
+ resourceCache.add(sampleFile, null).setInputComponent(new DefaultInputFile("foo", "src/Foo.php").setLines(5));
+ measureCache = mock(MeasureCache.class);
+ when(measureCache.byMetric(anyString(), anyString())).thenReturn(null);
+ publisher = new CoveragePublisher(resourceCache, measureCache);
+ }
+
+ @Test
+ public void publishCoverage() throws Exception {
+
+ Measure utLineHits = new Measure<>(CoreMetrics.COVERAGE_LINE_HITS_DATA).setData("2=1;3=1;5=0;6=3");
+ when(measureCache.byMetric("foo:src/Foo.php", CoreMetrics.COVERAGE_LINE_HITS_DATA_KEY)).thenReturn(utLineHits);
+
+ Measure conditionsByLine = new Measure<>(CoreMetrics.CONDITIONS_BY_LINE).setData("3=4");
+ when(measureCache.byMetric("foo:src/Foo.php", CoreMetrics.CONDITIONS_BY_LINE_KEY)).thenReturn(conditionsByLine);
+
+ Measure coveredConditionsByUts = new Measure<>(CoreMetrics.COVERED_CONDITIONS_BY_LINE).setData("3=2");
+ when(measureCache.byMetric("foo:src/Foo.php", CoreMetrics.COVERED_CONDITIONS_BY_LINE_KEY)).thenReturn(coveredConditionsByUts);
+
+ Measure itLineHits = new Measure<>(CoreMetrics.IT_COVERAGE_LINE_HITS_DATA).setData("2=0;3=0;5=1");
+ when(measureCache.byMetric("foo:src/Foo.php", CoreMetrics.IT_COVERAGE_LINE_HITS_DATA_KEY)).thenReturn(itLineHits);
+
+ Measure coveredConditionsByIts = new Measure<>(CoreMetrics.IT_COVERED_CONDITIONS_BY_LINE).setData("3=1");
+ when(measureCache.byMetric("foo:src/Foo.php", CoreMetrics.IT_COVERED_CONDITIONS_BY_LINE_KEY)).thenReturn(coveredConditionsByIts);
+
+ Measure overallCoveredConditions = new Measure<>(CoreMetrics.OVERALL_COVERED_CONDITIONS_BY_LINE).setData("3=2");
+ when(measureCache.byMetric("foo:src/Foo.php", CoreMetrics.OVERALL_COVERED_CONDITIONS_BY_LINE_KEY)).thenReturn(overallCoveredConditions);
+
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+
+ publisher.publish(writer);
+
+ try (CloseableIterator<Coverage> it = new ScannerReportReader(outputDir).readComponentCoverage(2)) {
+ assertThat(it.next()).isEqualTo(Coverage.newBuilder()
+ .setLine(2)
+ .setUtHits(true)
+ .setItHits(false)
+ .build());
+ assertThat(it.next()).isEqualTo(Coverage.newBuilder()
+ .setLine(3)
+ .setUtHits(true)
+ .setItHits(false)
+ .setConditions(4)
+ .setUtCoveredConditions(2)
+ .setItCoveredConditions(1)
+ .setOverallCoveredConditions(2)
+ .build());
+ assertThat(it.next()).isEqualTo(Coverage.newBuilder()
+ .setLine(5)
+ .setUtHits(false)
+ .setItHits(true)
+ .build());
+ }
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/MeasuresPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/MeasuresPublisherTest.java
new file mode 100644
index 00000000000..0c7a29dd377
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/MeasuresPublisherTest.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Date;
+import org.apache.commons.lang.exception.ExceptionUtils;
+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.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.core.metric.BatchMetrics;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MeasuresPublisherTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private MeasureCache measureCache;
+ private MeasuresPublisher publisher;
+
+ private org.sonar.api.resources.Resource sampleFile;
+
+ @Before
+ public void prepare() {
+ Project p = new Project("foo").setAnalysisDate(new Date(1234567L));
+ BatchComponentCache resourceCache = new BatchComponentCache();
+ sampleFile = org.sonar.api.resources.File.create("src/Foo.php").setEffectiveKey("foo:src/Foo.php");
+ resourceCache.add(p, null);
+ resourceCache.add(sampleFile, null);
+ measureCache = mock(MeasureCache.class);
+ when(measureCache.byResource(any(Resource.class))).thenReturn(Collections.<Measure>emptyList());
+ publisher = new MeasuresPublisher(resourceCache, measureCache, new BatchMetrics());
+ }
+
+ @Test
+ public void publishMeasures() throws Exception {
+ Measure measure = new Measure<>(CoreMetrics.LINES_TO_COVER)
+ .setValue(2.0);
+ // String value
+ Measure stringMeasure = new Measure<>(CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION)
+ .setData("foo bar");
+ when(measureCache.byResource(sampleFile)).thenReturn(asList(measure, stringMeasure));
+
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+
+ publisher.publish(writer);
+
+ ScannerReportReader reader = new ScannerReportReader(outputDir);
+
+ assertThat(reader.readComponentMeasures(1)).hasSize(0);
+ try (CloseableIterator<ScannerReport.Measure> componentMeasures = reader.readComponentMeasures(2)) {
+ assertThat(componentMeasures).hasSize(2);
+ }
+ }
+
+ @Test
+ public void fail_with_IAE_when_measure_has_no_value() throws Exception {
+ Measure measure = new Measure<>(CoreMetrics.LINES_TO_COVER);
+ when(measureCache.byResource(sampleFile)).thenReturn(Collections.singletonList(measure));
+
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+
+ try {
+ publisher.publish(writer);
+ fail();
+ } catch (RuntimeException e) {
+ assertThat(ExceptionUtils.getFullStackTrace(e)).contains("Measure on metric 'lines_to_cover' and component 'foo:src/Foo.php' has no value, but it's not allowed");
+ }
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/MetadataPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/MetadataPublisherTest.java
new file mode 100644
index 00000000000..7069845e9cf
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/MetadataPublisherTest.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.File;
+import java.util.Date;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportReader;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MetadataPublisherTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private ProjectDefinition projectDef;
+ private Project project;
+ private MetadataPublisher underTest;
+ private Settings settings;
+
+ @Before
+ public void prepare() {
+ projectDef = ProjectDefinition.create().setKey("foo");
+ project = new Project("foo").setAnalysisDate(new Date(1234567L));
+ BatchComponentCache componentCache = new BatchComponentCache();
+ org.sonar.api.resources.Resource sampleFile = org.sonar.api.resources.File.create("src/Foo.php").setEffectiveKey("foo:src/Foo.php");
+ componentCache.add(project, null);
+ componentCache.add(sampleFile, project);
+ settings = new Settings();
+ underTest = new MetadataPublisher(componentCache, new ImmutableProjectReactor(projectDef), settings);
+ }
+
+ @Test
+ public void write_metadata() throws Exception {
+ settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true");
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+
+ underTest.publish(writer);
+
+ ScannerReportReader reader = new ScannerReportReader(outputDir);
+ ScannerReport.Metadata metadata = reader.readMetadata();
+ assertThat(metadata.getAnalysisDate()).isEqualTo(1234567L);
+ assertThat(metadata.getProjectKey()).isEqualTo("foo");
+ assertThat(metadata.getProjectKey()).isEqualTo("foo");
+ assertThat(metadata.getCrossProjectDuplicationActivated()).isTrue();
+ }
+
+ @Test
+ public void write_project_branch() throws Exception {
+ settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true");
+ settings.setProperty(CoreProperties.PROJECT_BRANCH_PROPERTY, "myBranch");
+ projectDef.properties().put(CoreProperties.PROJECT_BRANCH_PROPERTY, "myBranch");
+ project.setKey("foo:myBranch");
+ project.setEffectiveKey("foo:myBranch");
+
+ File outputDir = temp.newFolder();
+ ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+
+ underTest.publish(writer);
+
+ ScannerReportReader reader = new ScannerReportReader(outputDir);
+ ScannerReport.Metadata metadata = reader.readMetadata();
+ assertThat(metadata.getAnalysisDate()).isEqualTo(1234567L);
+ assertThat(metadata.getProjectKey()).isEqualTo("foo");
+ assertThat(metadata.getBranch()).isEqualTo("myBranch");
+ // Cross project duplication disabled on branches
+ assertThat(metadata.getCrossProjectDuplicationActivated()).isFalse();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ReportPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ReportPublisherTest.java
new file mode 100644
index 00000000000..bb54bfb9113
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/ReportPublisherTest.java
@@ -0,0 +1,164 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.TempFolder;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.BatchWsClient;
+import org.sonar.batch.scan.ImmutableProjectReactor;
+import org.sonar.core.config.CorePropertyDefinitions;
+
+import static org.apache.commons.io.FileUtils.readFileToString;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ReportPublisherTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ DefaultAnalysisMode mode = mock(DefaultAnalysisMode.class);
+ Settings settings = new Settings(new PropertyDefinitions(CorePropertyDefinitions.all()));
+ BatchWsClient wsClient = mock(BatchWsClient.class, Mockito.RETURNS_DEEP_STUBS);
+ ImmutableProjectReactor reactor = mock(ImmutableProjectReactor.class);
+ ProjectDefinition root;
+ AnalysisContextReportPublisher contextPublisher = mock(AnalysisContextReportPublisher.class);
+
+ @Before
+ public void setUp() {
+ root = ProjectDefinition.create().setKey("struts").setWorkDir(temp.getRoot());
+ when(reactor.getRoot()).thenReturn(root);
+ when(wsClient.baseUrl()).thenReturn("https://localhost/");
+ }
+
+ @Test
+ public void log_and_dump_information_about_report_uploading() throws IOException {
+ ReportPublisher underTest = new ReportPublisher(settings, wsClient, contextPublisher, reactor, mode, mock(TempFolder.class), new ReportPublisherStep[0]);
+
+ underTest.logSuccess("TASK-123");
+
+ assertThat(logTester.logs(LoggerLevel.INFO))
+ .contains("ANALYSIS SUCCESSFUL, you can browse https://localhost/dashboard/index/struts")
+ .contains("Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report")
+ .contains("More about the report processing at https://localhost/api/ce/task?id=TASK-123");
+
+ File detailsFile = new File(temp.getRoot(), "report-task.txt");
+ assertThat(readFileToString(detailsFile)).isEqualTo(
+ "projectKey=struts\n" +
+ "serverUrl=https://localhost\n" +
+ "dashboardUrl=https://localhost/dashboard/index/struts\n" +
+ "ceTaskId=TASK-123\n" +
+ "ceTaskUrl=https://localhost/api/ce/task?id=TASK-123\n"
+ );
+ }
+
+ @Test
+ public void log_public_url_if_defined() throws IOException {
+ settings.setProperty(CoreProperties.SERVER_BASE_URL, "https://publicserver/sonarqube");
+ ReportPublisher underTest = new ReportPublisher(settings, wsClient, contextPublisher, reactor, mode, mock(TempFolder.class), new ReportPublisherStep[0]);
+
+ underTest.logSuccess("TASK-123");
+
+ assertThat(logTester.logs(LoggerLevel.INFO))
+ .contains("ANALYSIS SUCCESSFUL, you can browse https://publicserver/sonarqube/dashboard/index/struts")
+ .contains("More about the report processing at https://publicserver/sonarqube/api/ce/task?id=TASK-123");
+
+ File detailsFile = new File(temp.getRoot(), "report-task.txt");
+ assertThat(readFileToString(detailsFile)).isEqualTo(
+ "projectKey=struts\n" +
+ "serverUrl=https://publicserver/sonarqube\n" +
+ "dashboardUrl=https://publicserver/sonarqube/dashboard/index/struts\n" +
+ "ceTaskId=TASK-123\n" +
+ "ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123\n"
+ );
+ }
+
+ @Test
+ public void fail_if_public_url_malformed() throws IOException {
+ settings.setProperty(CoreProperties.SERVER_BASE_URL, "invalid");
+ ReportPublisher underTest = new ReportPublisher(settings, wsClient, contextPublisher, reactor, mode, mock(TempFolder.class), new ReportPublisherStep[0]);
+
+ exception.expect(MessageException.class);
+ exception.expectMessage("Failed to parse public URL set in SonarQube server: invalid");
+ underTest.start();
+ }
+
+ @Test
+ public void log_but_not_dump_information_when_report_is_not_uploaded() {
+ ReportPublisher underTest = new ReportPublisher(settings, wsClient, contextPublisher, reactor, mode, mock(TempFolder.class), new ReportPublisherStep[0]);
+
+ underTest.logSuccess(/* report not uploaded, no server task */null);
+
+ assertThat(logTester.logs(LoggerLevel.INFO))
+ .contains("ANALYSIS SUCCESSFUL")
+ .doesNotContain("dashboard/index");
+
+ File detailsFile = new File(temp.getRoot(), ReportPublisher.METADATA_DUMP_FILENAME);
+ assertThat(detailsFile).doesNotExist();
+ }
+
+ @Test
+ public void should_not_delete_report_if_property_is_set() throws IOException {
+ settings.setProperty("sonar.batch.keepReport", true);
+ Path reportDir = temp.getRoot().toPath().resolve("batch-report");
+ Files.createDirectory(reportDir);
+ ReportPublisher underTest = new ReportPublisher(settings, wsClient, contextPublisher, reactor, mode, mock(TempFolder.class), new ReportPublisherStep[0]);
+
+ underTest.start();
+ underTest.stop();
+ assertThat(reportDir).isDirectory();
+ }
+
+ @Test
+ public void should_delete_report_by_default() throws IOException {
+ Path reportDir = temp.getRoot().toPath().resolve("batch-report");
+ Files.createDirectory(reportDir);
+ ReportPublisher job = new ReportPublisher(settings, wsClient, contextPublisher, reactor, mode, mock(TempFolder.class), new ReportPublisherStep[0]);
+
+ job.start();
+ job.stop();
+ assertThat(reportDir).doesNotExist();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/report/SourcePublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/SourcePublisherTest.java
new file mode 100644
index 00000000000..683d6fecb8a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/report/SourcePublisherTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.report;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+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.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SourcePublisherTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private SourcePublisher publisher;
+
+ private File sourceFile;
+
+ private ScannerReportWriter writer;
+
+ private org.sonar.api.resources.File sampleFile;
+
+ @Before
+ public void prepare() throws IOException {
+ Project p = new Project("foo").setAnalysisDate(new Date(1234567L));
+ BatchComponentCache resourceCache = new BatchComponentCache();
+ sampleFile = org.sonar.api.resources.File.create("src/Foo.php");
+ sampleFile.setEffectiveKey("foo:src/Foo.php");
+ resourceCache.add(p, null).setInputComponent(new DefaultInputModule("foo"));
+ File baseDir = temp.newFolder();
+ sourceFile = new File(baseDir, "src/Foo.php");
+ resourceCache.add(sampleFile, null).setInputComponent(
+ new DefaultInputFile("foo", "src/Foo.php").setLines(5).setModuleBaseDir(baseDir.toPath()).setCharset(StandardCharsets.ISO_8859_1));
+ publisher = new SourcePublisher(resourceCache);
+ File outputDir = temp.newFolder();
+ writer = new ScannerReportWriter(outputDir);
+ }
+
+ @Test
+ public void publishEmptySource() throws Exception {
+ FileUtils.write(sourceFile, "", StandardCharsets.ISO_8859_1);
+
+ publisher.publish(writer);
+
+ File out = writer.getSourceFile(2);
+ assertThat(FileUtils.readFileToString(out, StandardCharsets.UTF_8)).isEqualTo("");
+ }
+
+ @Test
+ public void publishSourceWithLastEmptyLine() throws Exception {
+ FileUtils.write(sourceFile, "1\n2\n3\n4\n", StandardCharsets.ISO_8859_1);
+
+ publisher.publish(writer);
+
+ File out = writer.getSourceFile(2);
+ assertThat(FileUtils.readFileToString(out, StandardCharsets.UTF_8)).isEqualTo("1\n2\n3\n4\n");
+ }
+
+ @Test
+ public void publishTestSource() throws Exception {
+ FileUtils.write(sourceFile, "1\n2\n3\n4\n", StandardCharsets.ISO_8859_1);
+ sampleFile.setQualifier(Qualifiers.UNIT_TEST_FILE);
+
+ publisher.publish(writer);
+
+ File out = writer.getSourceFile(2);
+ assertThat(FileUtils.readFileToString(out, StandardCharsets.UTF_8)).isEqualTo("1\n2\n3\n4\n");
+ }
+
+ @Test
+ public void publishSourceWithLastLineNotEmpty() throws Exception {
+ FileUtils.write(sourceFile, "1\n2\n3\n4\n5", StandardCharsets.ISO_8859_1);
+
+ publisher.publish(writer);
+
+ File out = writer.getSourceFile(2);
+ assertThat(FileUtils.readFileToString(out, StandardCharsets.UTF_8)).isEqualTo("1\n2\n3\n4\n5");
+ }
+
+ @Test
+ public void cleanLineEnds() throws Exception {
+ FileUtils.write(sourceFile, "\n2\r\n3\n4\r5", StandardCharsets.ISO_8859_1);
+
+ publisher.publish(writer);
+
+ File out = writer.getSourceFile(2);
+ assertThat(FileUtils.readFileToString(out, StandardCharsets.UTF_8)).isEqualTo("\n2\n3\n4\n5");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoaderTest.java
new file mode 100644
index 00000000000..cb3781e5103
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultGlobalRepositoriesLoaderTest.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+
+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.when;
+
+public class DefaultGlobalRepositoriesLoaderTest {
+ private static final String BATCH_GLOBAL_URL = "/batch/global";
+ private WSLoader wsLoader;
+ private WSLoaderResult<String> result;
+ private DefaultGlobalRepositoriesLoader globalRepositoryLoader;
+
+ @Before
+ public void setUp() {
+ wsLoader = mock(WSLoader.class);
+ result = new WSLoaderResult<>(new GlobalRepositories().toJson(), true);
+ when(wsLoader.loadString(BATCH_GLOBAL_URL)).thenReturn(result);
+
+ globalRepositoryLoader = new DefaultGlobalRepositoriesLoader(wsLoader);
+ }
+
+ @Test
+ public void test() {
+ MutableBoolean fromCache = new MutableBoolean();
+ globalRepositoryLoader.load(fromCache);
+
+ assertThat(fromCache.booleanValue()).isTrue();
+ verify(wsLoader).loadString(BATCH_GLOBAL_URL);
+ verifyNoMoreInteractions(wsLoader);
+ }
+
+ @Test
+ public void testFromServer() {
+ result = new WSLoaderResult<>(new GlobalRepositories().toJson(), false);
+ when(wsLoader.loadString(BATCH_GLOBAL_URL)).thenReturn(result);
+ MutableBoolean fromCache = new MutableBoolean();
+ globalRepositoryLoader.load(fromCache);
+
+ assertThat(fromCache.booleanValue()).isFalse();
+ verify(wsLoader).loadString(BATCH_GLOBAL_URL);
+ verifyNoMoreInteractions(wsLoader);
+ }
+
+ public void testWithoutArg() {
+ globalRepositoryLoader.load(null);
+
+ verify(wsLoader).loadString(BATCH_GLOBAL_URL);
+ verifyNoMoreInteractions(wsLoader);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest.java
new file mode 100644
index 00000000000..d633820ca83
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest.java
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import com.google.common.io.Resources;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.cache.WSLoader;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonarqube.ws.WsBatch.WsProjectResponse;
+import org.sonarqube.ws.client.HttpException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DefaultProjectRepositoriesLoaderTest {
+ private final static String PROJECT_KEY = "foo?";
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private DefaultProjectRepositoriesLoader loader;
+ private WSLoader wsLoader;
+
+ @Before
+ public void prepare() throws IOException {
+ wsLoader = mock(WSLoader.class);
+ InputStream is = mockData();
+ when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true));
+ loader = new DefaultProjectRepositoriesLoader(wsLoader);
+ }
+
+ @Test
+ public void continueOnError() {
+ when(wsLoader.loadStream(anyString())).thenThrow(IllegalStateException.class);
+ ProjectRepositories proj = loader.load(PROJECT_KEY, false, null);
+ assertThat(proj.exists()).isEqualTo(false);
+ }
+
+ @Test
+ public void parsingError() throws IOException {
+ InputStream is = mock(InputStream.class);
+ when(is.read()).thenThrow(IOException.class);
+
+ when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, false));
+ loader.load(PROJECT_KEY, false, null);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void failFastHttpError() {
+ HttpException http = new HttpException("url", 403);
+ IllegalStateException e = new IllegalStateException("http error", http);
+ when(wsLoader.loadStream(anyString())).thenThrow(e);
+ loader.load(PROJECT_KEY, false, null);
+ }
+
+ @Test
+ public void failFastHttpErrorMessageException() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("http error");
+
+ HttpException http = new HttpException("uri", 403);
+ MessageException e = MessageException.of("http error", http);
+ when(wsLoader.loadStream(anyString())).thenThrow(e);
+ loader.load(PROJECT_KEY, false, null);
+ }
+
+ @Test
+ public void passIssuesModeParameter() {
+ loader.load(PROJECT_KEY, false, null);
+ verify(wsLoader).loadStream("/batch/project.protobuf?key=foo%3F");
+
+ loader.load(PROJECT_KEY, true, null);
+ verify(wsLoader).loadStream("/batch/project.protobuf?key=foo%3F&issues_mode=true");
+ }
+
+ @Test
+ public void deserializeResponse() throws IOException {
+ MutableBoolean fromCache = new MutableBoolean();
+ loader.load(PROJECT_KEY, false, fromCache);
+ assertThat(fromCache.booleanValue()).isTrue();
+ }
+
+ @Test
+ public void passAndEncodeProjectKeyParameter() {
+ loader.load(PROJECT_KEY, false, null);
+ verify(wsLoader).loadStream("/batch/project.protobuf?key=foo%3F");
+ }
+
+ private InputStream mockData() throws IOException {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ WsProjectResponse.Builder projectResponseBuilder = WsProjectResponse.newBuilder();
+ WsProjectResponse response = projectResponseBuilder.build();
+ response.writeTo(os);
+
+ return new ByteArrayInputStream(os.toByteArray());
+ }
+
+ @Test
+ public void readRealResponse() throws IOException {
+ InputStream is = getTestResource("project.protobuf");
+ when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true));
+
+ ProjectRepositories proj = loader.load("org.sonarsource.github:sonar-github-plugin", true, null);
+ FileData fd = proj.fileData("org.sonarsource.github:sonar-github-plugin",
+ "src/test/java/org/sonar/plugins/github/PullRequestIssuePostJobTest.java");
+
+ assertThat(fd.revision()).isEqualTo("27bf2c54633d05c5df402bbe09471fe43bd9e2e5");
+ assertThat(fd.hash()).isEqualTo("edb6b3b9ab92d8dc53ba90ab86cd422e");
+ }
+
+ private InputStream getTestResource(String name) throws IOException {
+ return Resources.asByteSource(this.getClass().getResource(this.getClass().getSimpleName() + "/" + name))
+ .openBufferedStream();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultQualityProfileLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultQualityProfileLoaderTest.java
new file mode 100644
index 00000000000..d3b4ce6b87d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultQualityProfileLoaderTest.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.sonar.api.utils.MessageException;
+
+import org.sonarqube.ws.QualityProfiles;
+import com.google.common.io.Resources;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultQualityProfileLoaderTest {
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ private DefaultQualityProfileLoader qpLoader;
+ private WSLoader ws;
+ private InputStream is;
+
+ @Before
+ public void setUp() throws IOException {
+ ws = mock(WSLoader.class);
+ is = mock(InputStream.class);
+ when(is.read()).thenReturn(-1);
+ WSLoaderResult<InputStream> result = new WSLoaderResult<>(is, false);
+ when(ws.loadStream(anyString())).thenReturn(result);
+ qpLoader = new DefaultQualityProfileLoader(ws);
+ }
+
+ @Test
+ public void testEncoding() throws IOException {
+ WSLoaderResult<InputStream> result = new WSLoaderResult<>(createEncodedQP("qp"), false);
+ when(ws.loadStream(anyString())).thenReturn(result);
+
+ List<QualityProfile> loaded = qpLoader.load("foo#2", "my-profile#2", null);
+ verify(ws).loadStream("/api/qualityprofiles/search.protobuf?projectKey=foo%232&profileName=my-profile%232");
+ verifyNoMoreInteractions(ws);
+ assertThat(loaded).hasSize(1);
+ }
+
+ @Test
+ public void testNoProfile() throws IOException {
+ InputStream is = createEncodedQP();
+ when(ws.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, false));
+
+ exception.expect(MessageException.class);
+ exception.expectMessage("No quality profiles");
+
+ qpLoader.load("project", null, null);
+ verifyNoMoreInteractions(ws);
+ }
+
+ @Test
+ public void use_real_response() throws IOException {
+ InputStream is = getTestResource("quality_profile_search_default");
+ when(ws.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, false));
+
+ List<QualityProfile> loaded = qpLoader.loadDefault(null, null);
+ verify(ws).loadStream("/api/qualityprofiles/search.protobuf?defaults=true");
+ verifyNoMoreInteractions(ws);
+ assertThat(loaded).hasSize(1);
+ }
+
+ private InputStream getTestResource(String name) throws IOException {
+ return Resources.asByteSource(this.getClass().getResource(this.getClass().getSimpleName() + "/" + name))
+ .openBufferedStream();
+ }
+
+ private static InputStream createEncodedQP(String... names) throws IOException {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ QualityProfiles.SearchWsResponse.Builder responseBuilder = QualityProfiles.SearchWsResponse.newBuilder();
+
+ for (String n : names) {
+ QualityProfile qp = QualityProfile.newBuilder().setKey(n).setName(n).setLanguage("lang").build();
+ responseBuilder.addProfiles(qp);
+ }
+
+ responseBuilder.build().writeTo(os);
+ return new ByteArrayInputStream(os.toByteArray());
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java
new file mode 100644
index 00000000000..22416368f3f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.scanner.protocol.input.ScannerInput;
+import org.sonar.scanner.protocol.input.ScannerInput.ServerIssue;
+import org.sonar.batch.cache.WSLoader;
+import com.google.common.base.Function;
+import org.junit.Before;
+import org.junit.Test;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultServerIssuesLoaderTest {
+ private DefaultServerIssuesLoader loader;
+ private WSLoader wsLoader;
+
+ @Before
+ public void prepare() {
+ wsLoader = mock(WSLoader.class);
+ loader = new DefaultServerIssuesLoader(wsLoader);
+ }
+
+ @Test
+ public void loadFromWs() throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+ ServerIssue.newBuilder().setKey("ab1").build()
+ .writeDelimitedTo(bos);
+ ServerIssue.newBuilder().setKey("ab2").build()
+ .writeDelimitedTo(bos);
+
+ InputStream is = new ByteArrayInputStream(bos.toByteArray());
+ when(wsLoader.loadStream("/batch/issues.protobuf?key=foo")).thenReturn(new WSLoaderResult<>(is, true));
+
+ final List<ServerIssue> result = new ArrayList<>();
+ loader.load("foo", new Function<ScannerInput.ServerIssue, Void>() {
+
+ @Override
+ public Void apply(ServerIssue input) {
+ result.add(input);
+ return null;
+ }
+ });
+
+ assertThat(result).extracting("key").containsExactly("ab1", "ab2");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testError() throws IOException {
+ InputStream is = mock(InputStream.class);
+ when(is.read()).thenThrow(IOException.class);
+ when(wsLoader.loadStream("/batch/issues.protobuf?key=foo")).thenReturn(new WSLoaderResult<>(is, true));
+ loader.load("foo", mock(Function.class));
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/ProjectRepositoriesProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/ProjectRepositoriesProviderTest.java
new file mode 100644
index 00000000000..781b48a2a59
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/ProjectRepositoriesProviderTest.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import java.util.Date;
+
+import org.sonar.batch.repository.FileData;
+import com.google.common.collect.Table;
+import com.google.common.collect.HashBasedTable;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.api.batch.bootstrap.ProjectKey;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class ProjectRepositoriesProviderTest {
+ private ProjectRepositoriesProvider provider;
+ private ProjectRepositories project;
+
+ @Mock
+ private ProjectRepositoriesLoader loader;
+ @Mock
+ private ProjectKey projectKey;
+ @Mock
+ private DefaultAnalysisMode mode;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ Table<String, String, String> t1 = HashBasedTable.create();
+ Table<String, String, FileData> t2 = HashBasedTable.create();
+
+ project = new ProjectRepositories(t1, t2, new Date());
+ provider = new ProjectRepositoriesProvider();
+
+ when(projectKey.get()).thenReturn("key");
+ }
+
+ @Test
+ public void testNonAssociated() {
+ when(mode.isNotAssociated()).thenReturn(true);
+ ProjectRepositories repo = provider.provide(loader, projectKey, mode);
+
+ assertThat(repo.exists()).isEqualTo(false);
+ verify(mode).isNotAssociated();
+ verifyNoMoreInteractions(loader, projectKey, mode);
+ }
+
+ @Test
+ public void singleton() {
+ when(mode.isNotAssociated()).thenReturn(true);
+ ProjectRepositories repo = provider.provide(loader, projectKey, mode);
+
+ assertThat(repo.exists()).isEqualTo(false);
+ verify(mode).isNotAssociated();
+ verifyNoMoreInteractions(loader, projectKey, mode);
+
+ repo = provider.provide(loader, projectKey, mode);
+ verifyNoMoreInteractions(loader, projectKey, mode);
+ }
+
+ @Test
+ public void testValidation() {
+ when(mode.isNotAssociated()).thenReturn(false);
+ when(mode.isIssues()).thenReturn(true);
+ when(loader.load(eq("key"), eq(true), any(MutableBoolean.class))).thenReturn(project);
+
+ provider.provide(loader, projectKey, mode);
+ }
+
+ @Test
+ public void testAssociated() {
+ when(mode.isNotAssociated()).thenReturn(false);
+ when(mode.isIssues()).thenReturn(false);
+ when(loader.load(eq("key"), eq(false), any(MutableBoolean.class))).thenReturn(project);
+
+ ProjectRepositories repo = provider.provide(loader, projectKey, mode);
+
+ assertThat(repo.exists()).isEqualTo(true);
+ assertThat(repo.lastAnalysisDate()).isNotNull();
+
+ verify(mode).isNotAssociated();
+ verify(mode, times(2)).isIssues();
+ verify(projectKey).get();
+ verify(loader).load(eq("key"), eq(false), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader, projectKey, mode);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/QualityProfileProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/QualityProfileProviderTest.java
new file mode 100644
index 00000000000..0cd1683b9c9
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/QualityProfileProviderTest.java
@@ -0,0 +1,165 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.api.batch.bootstrap.ProjectKey;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.batch.analysis.AnalysisProperties;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.rule.ModuleQProfiles;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class QualityProfileProviderTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private QualityProfileProvider qualityProfileProvider;
+
+ @Mock
+ private QualityProfileLoader loader;
+ @Mock
+ private DefaultAnalysisMode mode;
+ @Mock
+ private AnalysisProperties props;
+ @Mock
+ private ProjectKey key;
+ @Mock
+ private ProjectRepositories projectRepo;
+
+ private List<QualityProfile> response;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ qualityProfileProvider = new QualityProfileProvider();
+
+ when(key.get()).thenReturn("project");
+ when(projectRepo.exists()).thenReturn(true);
+
+ response = new ArrayList<>(1);
+ response.add(QualityProfile.newBuilder().setKey("profile").setName("profile").setLanguage("lang").build());
+ }
+
+ @Test
+ public void testProvide() {
+ when(mode.isNotAssociated()).thenReturn(false);
+ when(loader.load(eq("project"), isNull(String.class), any(MutableBoolean.class))).thenReturn(response);
+ ModuleQProfiles qps = qualityProfileProvider.provide(key, loader, projectRepo, props, mode);
+ assertResponse(qps);
+
+ verify(loader).load(eq("project"), isNull(String.class), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader);
+ }
+
+ @Test
+ public void testNonAssociated() {
+ when(mode.isNotAssociated()).thenReturn(true);
+ when(loader.loadDefault(anyString(), any(MutableBoolean.class))).thenReturn(response);
+ ModuleQProfiles qps = qualityProfileProvider.provide(key, loader, projectRepo, props, mode);
+ assertResponse(qps);
+
+ verify(loader).loadDefault(anyString(), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader);
+ }
+
+ @Test
+ public void testProjectDoesntExist() {
+ when(mode.isNotAssociated()).thenReturn(false);
+ when(projectRepo.exists()).thenReturn(false);
+ when(loader.loadDefault(anyString(), any(MutableBoolean.class))).thenReturn(response);
+ ModuleQProfiles qps = qualityProfileProvider.provide(key, loader, projectRepo, props, mode);
+ assertResponse(qps);
+
+ verify(loader).loadDefault(anyString(), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader);
+ }
+
+ @Test
+ public void testProfileProp() {
+ when(mode.isNotAssociated()).thenReturn(false);
+ when(loader.load(eq("project"), eq("custom"), any(MutableBoolean.class))).thenReturn(response);
+ when(props.property(ModuleQProfiles.SONAR_PROFILE_PROP)).thenReturn("custom");
+ when(props.properties()).thenReturn(ImmutableMap.of(ModuleQProfiles.SONAR_PROFILE_PROP, "custom"));
+
+ ModuleQProfiles qps = qualityProfileProvider.provide(key, loader, projectRepo, props, mode);
+ assertResponse(qps);
+
+ verify(loader).load(eq("project"), eq("custom"), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader);
+ assertThat(logTester.logs(LoggerLevel.WARN)).contains("Ability to set quality profile from command line using '" + ModuleQProfiles.SONAR_PROFILE_PROP
+ + "' is deprecated and will be dropped in a future SonarQube version. Please configure quality profile used by your project on SonarQube server.");
+ }
+
+ @Test
+ public void testIgnoreSonarProfileIssuesMode() {
+ when(mode.isNotAssociated()).thenReturn(false);
+ when(mode.isIssues()).thenReturn(true);
+ when(loader.load(eq("project"), (String) eq(null), any(MutableBoolean.class))).thenReturn(response);
+ when(props.property(ModuleQProfiles.SONAR_PROFILE_PROP)).thenReturn("custom");
+
+ ModuleQProfiles qps = qualityProfileProvider.provide(key, loader, projectRepo, props, mode);
+ assertResponse(qps);
+
+ verify(loader).load(eq("project"), (String) eq(null), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader);
+ }
+
+ @Test
+ public void testProfilePropDefault() {
+ when(mode.isNotAssociated()).thenReturn(true);
+ when(loader.loadDefault(eq("custom"), any(MutableBoolean.class))).thenReturn(response);
+ when(props.property(ModuleQProfiles.SONAR_PROFILE_PROP)).thenReturn("custom");
+ when(props.properties()).thenReturn(ImmutableMap.of(ModuleQProfiles.SONAR_PROFILE_PROP, "custom"));
+
+ ModuleQProfiles qps = qualityProfileProvider.provide(key, loader, projectRepo, props, mode);
+ assertResponse(qps);
+
+ verify(loader).loadDefault(eq("custom"), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader);
+ assertThat(logTester.logs(LoggerLevel.WARN)).contains("Ability to set quality profile from command line using '" + ModuleQProfiles.SONAR_PROFILE_PROP
+ + "' is deprecated and will be dropped in a future SonarQube version. Please configure quality profile used by your project on SonarQube server.");
+ }
+
+ private void assertResponse(ModuleQProfiles qps) {
+ assertThat(qps.findAll()).hasSize(1);
+ assertThat(qps.findAll()).extracting("key").containsExactly("profile");
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java
new file mode 100644
index 00000000000..870afd5737e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.repository.user;
+
+import org.assertj.core.util.Lists;
+
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.scanner.protocol.input.ScannerInput;
+import org.sonar.batch.cache.WSLoader;
+import org.junit.Before;
+import com.google.common.collect.ImmutableList;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import com.google.common.collect.ImmutableMap;
+import org.junit.rules.ExpectedException;
+import org.junit.Rule;
+import org.mockito.Mockito;
+import org.junit.Test;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Map;
+
+import static org.mockito.Matchers.anyString;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class UserRepositoryLoaderTest {
+ @Rule
+ public final ExpectedException exception = ExpectedException.none();
+
+ private WSLoader wsLoader;
+ private UserRepositoryLoader userRepo;
+
+ @Before
+ public void setUp() {
+ wsLoader = mock(WSLoader.class);
+ userRepo = new UserRepositoryLoader(wsLoader);
+ }
+
+ @Test
+ public void testLoadEmptyList() {
+ assertThat(userRepo.load(Lists.<String>emptyList())).isEmpty();
+ }
+
+ @Test
+ public void testLoad() throws IOException {
+ Map<String, String> userMap = ImmutableMap.of("fmallet", "Freddy Mallet", "sbrandhof", "Simon");
+ WSLoaderResult<InputStream> res = new WSLoaderResult<>(createUsersMock(userMap), true);
+ when(wsLoader.loadStream("/batch/users?logins=fmallet,sbrandhof")).thenReturn(res);
+
+ assertThat(userRepo.load(Arrays.asList("fmallet", "sbrandhof"))).extracting("login", "name").containsOnly(tuple("fmallet", "Freddy Mallet"), tuple("sbrandhof", "Simon"));
+ }
+
+ @Test
+ public void testFromCache() throws IOException {
+ WSLoaderResult<InputStream> res = new WSLoaderResult<>(createUsersMock(ImmutableMap.of("fmallet", "Freddy Mallet")), true);
+ when(wsLoader.loadStream(anyString())).thenReturn(res);
+ MutableBoolean fromCache = new MutableBoolean();
+ userRepo.load("", fromCache);
+ assertThat(fromCache.booleanValue()).isTrue();
+
+ fromCache.setValue(false);
+ userRepo.load(ImmutableList.of("user"), fromCache);
+ assertThat(fromCache.booleanValue()).isTrue();
+ }
+
+ @Test
+ public void testLoadSingleUser() throws IOException {
+ WSLoaderResult<InputStream> res = new WSLoaderResult<>(createUsersMock(ImmutableMap.of("fmallet", "Freddy Mallet")), true);
+ when(wsLoader.loadStream("/batch/users?logins=fmallet")).thenReturn(res);
+
+ assertThat(userRepo.load("fmallet").getName()).isEqualTo("Freddy Mallet");
+ }
+
+ private InputStream createUsersMock(Map<String, String> users) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ for (Map.Entry<String, String> user : users.entrySet()) {
+ ScannerInput.User.Builder builder = ScannerInput.User.newBuilder();
+ builder.setLogin(user.getKey()).setName(user.getValue()).build().writeDelimitedTo(out);
+ }
+ return new ByteArrayInputStream(out.toByteArray());
+ }
+
+ @Test
+ public void testInputStreamError() throws IOException {
+ InputStream is = mock(InputStream.class);
+ Mockito.doThrow(IOException.class).when(is).read();
+ WSLoaderResult<InputStream> res = new WSLoaderResult<>(is, true);
+
+ when(wsLoader.loadStream("/batch/users?logins=fmallet,sbrandhof")).thenReturn(res);
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Unable to get user details from server");
+
+ userRepo.load(Arrays.asList("fmallet", "sbrandhof"));
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/ActiveRulesProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/ActiveRulesProviderTest.java
new file mode 100644
index 00000000000..1e1b1f0fc3c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/ActiveRulesProviderTest.java
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import com.google.common.collect.ImmutableList;
+import java.util.LinkedList;
+import java.util.List;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.rule.RuleKey;
+import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class ActiveRulesProviderTest {
+ private ActiveRulesProvider provider;
+
+ @Mock
+ private DefaultActiveRulesLoader loader;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ provider = new ActiveRulesProvider();
+ }
+
+ @Test
+ public void testCombinationOfRules() {
+ LoadedActiveRule r1 = mockRule("rule1");
+ LoadedActiveRule r2 = mockRule("rule2");
+ LoadedActiveRule r3 = mockRule("rule3");
+
+ List<LoadedActiveRule> qp1Rules = ImmutableList.of(r1, r2);
+ List<LoadedActiveRule> qp2Rules = ImmutableList.of(r2, r3);
+ List<LoadedActiveRule> qp3Rules = ImmutableList.of(r1, r3);
+
+ when(loader.load(eq("qp1"), any(MutableBoolean.class))).thenReturn(qp1Rules);
+ when(loader.load(eq("qp2"), any(MutableBoolean.class))).thenReturn(qp2Rules);
+ when(loader.load(eq("qp3"), any(MutableBoolean.class))).thenReturn(qp3Rules);
+
+ ModuleQProfiles profiles = mockProfiles("qp1", "qp2", "qp3");
+ ActiveRules activeRules = provider.provide(loader, profiles);
+
+ assertThat(activeRules.findAll()).hasSize(3);
+ assertThat(activeRules.findAll()).extracting("ruleKey").containsOnly(
+ RuleKey.of("rule1", "rule1"), RuleKey.of("rule2", "rule2"), RuleKey.of("rule3", "rule3"));
+
+ verify(loader).load(eq("qp1"), any(MutableBoolean.class));
+ verify(loader).load(eq("qp2"), any(MutableBoolean.class));
+ verify(loader).load(eq("qp3"), any(MutableBoolean.class));
+ verifyNoMoreInteractions(loader);
+ }
+
+ private static ModuleQProfiles mockProfiles(String... keys) {
+ List<QualityProfile> profiles = new LinkedList<>();
+
+ for (String k : keys) {
+ QualityProfile p = QualityProfile.newBuilder().setKey(k).setLanguage(k).build();
+ profiles.add(p);
+ }
+
+ return new ModuleQProfiles(profiles);
+ }
+
+ private static LoadedActiveRule mockRule(String name) {
+ LoadedActiveRule r = new LoadedActiveRule();
+ r.setName(name);
+ r.setRuleKey(RuleKey.of(name, name));
+ return r;
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultActiveRulesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultActiveRulesLoaderTest.java
new file mode 100644
index 00000000000..434c84dda93
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultActiveRulesLoaderTest.java
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+import com.google.common.io.Resources;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+
+import static org.mockito.Mockito.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import org.junit.Before;
+
+public class DefaultActiveRulesLoaderTest {
+ private DefaultActiveRulesLoader loader;
+ private WSLoader ws;
+
+ @Before
+ public void setUp() {
+ ws = mock(WSLoader.class);
+ loader = new DefaultActiveRulesLoader(ws);
+ }
+
+ @Test
+ public void feed_real_response_encode_qp() throws IOException {
+ InputStream response1 = loadResource("active_rule_search1.protobuf");
+ InputStream response2 = loadResource("active_rule_search2.protobuf");
+
+ String req1 = "/api/rules/search.protobuf?f=repo,name,severity,lang,internalKey,templateKey,params,actives&activation=true&qprofile=c%2B-test_c%2B-values-17445&p=1&ps=500";
+ String req2 = "/api/rules/search.protobuf?f=repo,name,severity,lang,internalKey,templateKey,params,actives&activation=true&qprofile=c%2B-test_c%2B-values-17445&p=2&ps=500";
+ when(ws.loadStream(req1)).thenReturn(new WSLoaderResult<>(response1, false));
+ when(ws.loadStream(req2)).thenReturn(new WSLoaderResult<>(response2, false));
+
+ Collection<LoadedActiveRule> activeRules = loader.load("c+-test_c+-values-17445", null);
+ assertThat(activeRules).hasSize(226);
+ assertActiveRule(activeRules);
+
+ verify(ws).loadStream(req1);
+ verify(ws).loadStream(req2);
+ verifyNoMoreInteractions(ws);
+ }
+
+ private static void assertActiveRule(Collection<LoadedActiveRule> activeRules) {
+ RuleKey key = RuleKey.of("squid", "S3008");
+ for (LoadedActiveRule r : activeRules) {
+ if (!r.getRuleKey().equals(key)) {
+ continue;
+ }
+
+ assertThat(r.getParams().get("format")).isEqualTo("^[a-z][a-zA-Z0-9]*$");
+ assertThat(r.getSeverity()).isEqualTo("MINOR");
+ }
+ }
+
+ private InputStream loadResource(String name) throws IOException {
+ return Resources.asByteSource(this.getClass().getResource("DefaultActiveRulesLoaderTest/" + name))
+ .openBufferedStream();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java
new file mode 100644
index 00000000000..a5a461c4e9f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import org.junit.rules.ExpectedException;
+import org.sonar.batch.cache.WSLoaderResult;
+import org.sonar.batch.cache.WSLoader;
+import org.apache.commons.lang.mutable.MutableBoolean;
+import org.sonarqube.ws.Rules.ListResponse.Rule;
+import com.google.common.io.ByteSource;
+import com.google.common.io.Resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import static org.mockito.Matchers.anyString;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Test;
+
+public class DefaultRulesLoaderTest {
+ @org.junit.Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void testParseServerResponse() throws IOException {
+ WSLoader wsLoader = mock(WSLoader.class);
+ InputStream is = Resources.asByteSource(this.getClass().getResource("DefaultRulesLoader/response.protobuf")).openBufferedStream();
+ when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true));
+ DefaultRulesLoader loader = new DefaultRulesLoader(wsLoader);
+ List<Rule> ruleList = loader.load(null);
+ assertThat(ruleList).hasSize(318);
+ }
+
+ @Test
+ public void testLoadedFromCache() throws IOException {
+ WSLoader wsLoader = mock(WSLoader.class);
+ InputStream is = Resources.asByteSource(this.getClass().getResource("DefaultRulesLoader/response.protobuf")).openBufferedStream();
+ when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true));
+ DefaultRulesLoader loader = new DefaultRulesLoader(wsLoader);
+ MutableBoolean fromCache = new MutableBoolean();
+ loader.load(fromCache);
+
+ assertThat(fromCache.booleanValue()).isTrue();
+ }
+
+ @Test
+ public void testError() throws IOException {
+ WSLoader wsLoader = mock(WSLoader.class);
+ InputStream is = ByteSource.wrap(new String("trash").getBytes()).openBufferedStream();
+ when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true));
+ DefaultRulesLoader loader = new DefaultRulesLoader(wsLoader);
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Unable to get rules");
+
+ loader.load(null);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileSensorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileSensorTest.java
new file mode 100644
index 00000000000..4801f246c55
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileSensorTest.java
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.resources.Project;
+import org.sonar.api.test.IsMeasure;
+import org.sonar.core.util.UtcDateUtils;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class QProfileSensorTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ static final Date DATE = UtcDateUtils.parseDateTime("2014-01-15T12:00:00+0000");
+ static final QProfile JAVA_PROFILE = new QProfile().setKey("java-two").setName("Java Two").setLanguage("java")
+ .setRulesUpdatedAt(DATE);
+ static final QProfile PHP_PROFILE = new QProfile().setKey("php-one").setName("Php One").setLanguage("php")
+ .setRulesUpdatedAt(DATE);
+
+ ModuleQProfiles moduleQProfiles = mock(ModuleQProfiles.class);
+ Project project = mock(Project.class);
+ SensorContext sensorContext = mock(SensorContext.class);
+ DefaultFileSystem fs;
+
+ @Before
+ public void prepare() throws Exception {
+ fs = new DefaultFileSystem(temp.newFolder().toPath());
+ }
+
+ @Test
+ public void to_string() {
+ QProfileSensor sensor = new QProfileSensor(moduleQProfiles, fs, mock(AnalysisMode.class));
+ assertThat(sensor.toString()).isEqualTo("QProfileSensor");
+ }
+
+ @Test
+ public void no_execution_in_issues_mode() {
+ AnalysisMode analysisMode = mock(AnalysisMode.class);
+ when(analysisMode.isIssues()).thenReturn(true);
+ QProfileSensor sensor = new QProfileSensor(moduleQProfiles, fs, analysisMode);
+ assertThat(sensor.shouldExecuteOnProject(project)).isFalse();
+
+ }
+
+ @Test
+ public void no_qprofiles() {
+ when(moduleQProfiles.findAll()).thenReturn(Collections.<QProfile>emptyList());
+
+ QProfileSensor sensor = new QProfileSensor(moduleQProfiles, fs, mock(AnalysisMode.class));
+ assertThat(sensor.shouldExecuteOnProject(project)).isTrue();
+ sensor.analyse(project, sensorContext);
+
+ // measures are not saved
+ verify(sensorContext).saveMeasure(argThat(new IsMeasure(CoreMetrics.QUALITY_PROFILES, "[]")));
+ }
+
+ @Test
+ public void mark_profiles_as_used() {
+ when(moduleQProfiles.findByLanguage("java")).thenReturn(JAVA_PROFILE);
+ when(moduleQProfiles.findByLanguage("php")).thenReturn(PHP_PROFILE);
+ when(moduleQProfiles.findByLanguage("abap")).thenReturn(null);
+ fs.addLanguages("java", "php", "abap");
+
+ QProfileSensor sensor = new QProfileSensor(moduleQProfiles, fs, mock(AnalysisMode.class));
+ assertThat(sensor.shouldExecuteOnProject(project)).isTrue();
+ sensor.analyse(project, sensorContext);
+ }
+
+ @Test
+ public void store_measures_on_single_lang_module() {
+ when(moduleQProfiles.findByLanguage("java")).thenReturn(JAVA_PROFILE);
+ when(moduleQProfiles.findByLanguage("php")).thenReturn(PHP_PROFILE);
+ when(moduleQProfiles.findByLanguage("abap")).thenReturn(null);
+ fs.addLanguages("java");
+
+ QProfileSensor sensor = new QProfileSensor(moduleQProfiles, fs, mock(AnalysisMode.class));
+ assertThat(sensor.shouldExecuteOnProject(project)).isTrue();
+ sensor.analyse(project, sensorContext);
+
+ verify(sensorContext).saveMeasure(
+ argThat(new IsMeasure(CoreMetrics.QUALITY_PROFILES,
+ "[{\"key\":\"java-two\",\"language\":\"java\",\"name\":\"Java Two\",\"rulesUpdatedAt\":\"2014-01-15T12:00:00+0000\"}]")));
+ }
+
+ @Test
+ public void store_measures_on_multi_lang_module() {
+ when(moduleQProfiles.findByLanguage("java")).thenReturn(JAVA_PROFILE);
+ when(moduleQProfiles.findByLanguage("php")).thenReturn(PHP_PROFILE);
+ when(moduleQProfiles.findByLanguage("abap")).thenReturn(null);
+ fs.addLanguages("java", "php");
+
+ QProfileSensor sensor = new QProfileSensor(moduleQProfiles, fs, mock(AnalysisMode.class));
+ assertThat(sensor.shouldExecuteOnProject(project)).isTrue();
+ sensor.analyse(project, sensorContext);
+
+ verify(sensorContext).saveMeasure(
+ argThat(new IsMeasure(CoreMetrics.QUALITY_PROFILES,
+ "[{\"key\":\"java-two\",\"language\":\"java\",\"name\":\"Java Two\",\"rulesUpdatedAt\":\"2014-01-15T12:00:00+0000\"}," +
+ "{\"key\":\"php-one\",\"language\":\"php\",\"name\":\"Php One\",\"rulesUpdatedAt\":\"2014-01-15T12:00:00+0000\"}]")));
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileVerifierTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileVerifierTest.java
new file mode 100644
index 00000000000..9df8cd75491
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/QProfileVerifierTest.java
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.slf4j.Logger;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.MessageException;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class QProfileVerifierTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private DefaultFileSystem fs;
+ private ModuleQProfiles profiles;
+ private Settings settings = new Settings();
+
+ @Before
+ public void before() throws Exception {
+ fs = new DefaultFileSystem(temp.newFolder().toPath());
+ profiles = mock(ModuleQProfiles.class);
+ QProfile javaProfile = new QProfile().setKey("p1").setName("My Java profile").setLanguage("java");
+ when(profiles.findByLanguage("java")).thenReturn(javaProfile);
+ QProfile cobolProfile = new QProfile().setKey("p2").setName("My Cobol profile").setLanguage("cobol");
+ when(profiles.findByLanguage("cobol")).thenReturn(cobolProfile);
+ }
+
+ @Test
+ public void should_log_all_used_profiles() {
+ fs.addLanguages("java", "cobol");
+ QProfileVerifier profileLogger = new QProfileVerifier(settings, fs, profiles);
+ Logger logger = mock(Logger.class);
+ profileLogger.execute(logger);
+
+ verify(logger).info("Quality profile for {}: {}", "java", "My Java profile");
+ verify(logger).info("Quality profile for {}: {}", "cobol", "My Cobol profile");
+ }
+
+ @Test
+ public void should_fail_if_default_profile_not_used() {
+ fs.addLanguages("java", "cobol");
+ settings.setProperty("sonar.profile", "Unknown");
+
+ QProfileVerifier profileLogger = new QProfileVerifier(settings, fs, profiles);
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("sonar.profile was set to 'Unknown' but didn't match any profile for any language. Please check your configuration.");
+
+ profileLogger.execute();
+ }
+
+ @Test
+ public void should_not_fail_if_no_language_on_project() {
+ settings.setProperty("sonar.profile", "Unknown");
+
+ QProfileVerifier profileLogger = new QProfileVerifier(settings, fs, profiles);
+
+ profileLogger.execute();
+
+ }
+
+ @Test
+ public void should_not_fail_if_default_profile_used_at_least_once() {
+ fs.addLanguages("java", "cobol");
+ settings.setProperty("sonar.profile", "My Java profile");
+
+ QProfileVerifier profileLogger = new QProfileVerifier(settings, fs, profiles);
+
+ profileLogger.execute();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RuleFinderCompatibilityTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RuleFinderCompatibilityTest.java
new file mode 100644
index 00000000000..abc5f5dfd75
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RuleFinderCompatibilityTest.java
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.sonar.api.batch.rule.internal.RulesBuilder;
+
+import org.sonar.api.batch.rule.Rules;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleQuery;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RuleFinderCompatibilityTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private Rules rules;
+ private RuleFinderCompatibility ruleFinder;
+
+ @Before
+ public void prepare() {
+ RulesBuilder builder = new RulesBuilder();
+ builder.add(RuleKey.of("repo1", "rule1"));
+ builder.add(RuleKey.of("repo1", "rule2")).setInternalKey("rule2_internal");
+ builder.add(RuleKey.of("repo2", "rule1"));
+ rules = builder.build();
+
+ ruleFinder = new RuleFinderCompatibility(rules);
+ }
+
+ @Test
+ public void testByInternalKey() {
+ assertThat(ruleFinder.find(RuleQuery.create().withRepositoryKey("repo1").withConfigKey("rule2_internal")).getKey()).isEqualTo("rule2");
+ assertThat(ruleFinder.find(RuleQuery.create().withRepositoryKey("repo1").withConfigKey("rule2_internal2"))).isNull();
+ }
+
+ @Test
+ public void testByKey() {
+ assertThat(ruleFinder.find(RuleQuery.create().withRepositoryKey("repo1").withKey("rule2")).getKey()).isEqualTo("rule2");
+ assertThat(ruleFinder.find(RuleQuery.create().withRepositoryKey("repo1").withKey("rule3"))).isNull();
+ assertThat(ruleFinder.findByKey("repo1", "rule2").getKey()).isEqualTo("rule2");
+ }
+
+ @Test
+ public void duplicateResult() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Non unique result for rule query: RuleQuery[repositoryKey=repo1,key=<null>,configKey=<null>]");
+ ruleFinder.find(RuleQuery.create().withRepositoryKey("repo1"));
+ }
+
+ @Test
+ public void unsupportedById() {
+ thrown.expect(UnsupportedOperationException.class);
+ ruleFinder.findById(1);
+ }
+
+ @Test
+ public void unsupportedByInternalKeyWithoutRepo() {
+ thrown.expect(UnsupportedOperationException.class);
+ ruleFinder.find(RuleQuery.create().withConfigKey("config"));
+ }
+
+ @Test
+ public void unsupportedByKeyWithoutRepo() {
+ thrown.expect(UnsupportedOperationException.class);
+ ruleFinder.find(RuleQuery.create().withKey("key"));
+ }
+
+ @Test
+ public void unsupportedByKeyAndInternalKey() {
+ thrown.expect(UnsupportedOperationException.class);
+ ruleFinder.find(RuleQuery.create().withRepositoryKey("repo").withKey("key").withConfigKey("config"));
+ }
+
+ @Test
+ public void unsupportedByKeyAndInternalKeyWithoutRepo() {
+ thrown.expect(UnsupportedOperationException.class);
+ ruleFinder.find(RuleQuery.create().withKey("key").withConfigKey("config"));
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProfileProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProfileProviderTest.java
new file mode 100644
index 00000000000..208a187a966
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProfileProviderTest.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import java.util.Arrays;
+import org.junit.Test;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.config.Settings;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.rule.RuleKey;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class RulesProfileProviderTest {
+
+ ModuleQProfiles qProfiles = mock(ModuleQProfiles.class);
+ Settings settings = new Settings();
+ RulesProfileProvider provider = new RulesProfileProvider();
+
+ @Test
+ public void merge_profiles() {
+ QProfile qProfile = new QProfile().setKey("java-sw").setName("Sonar way").setLanguage("java");
+ when(qProfiles.findAll()).thenReturn(Arrays.asList(qProfile));
+
+ RulesProfile profile = provider.provide(qProfiles, new ActiveRulesBuilder().build(), settings);
+
+ // merge of all profiles
+ assertThat(profile).isNotNull().isInstanceOf(RulesProfileWrapper.class);
+ assertThat(profile.getLanguage()).isEqualTo("");
+ assertThat(profile.getName()).isEqualTo("SonarQube");
+ assertThat(profile.getActiveRules()).isEmpty();
+ try {
+ profile.getId();
+ fail();
+ } catch (IllegalStateException e) {
+ // id must not be used at all
+ }
+ }
+
+ @Test
+ public void keep_compatibility_with_single_language_projects() {
+ settings.setProperty("sonar.language", "java");
+
+ QProfile qProfile = new QProfile().setKey("java-sw").setName("Sonar way").setLanguage("java");
+ when(qProfiles.findByLanguage("java")).thenReturn(qProfile);
+
+ RulesProfile profile = provider.provide(qProfiles, new ActiveRulesBuilder().build(), settings);
+
+ // no merge, directly the old hibernate profile
+ assertThat(profile).isNotNull();
+ assertThat(profile.getLanguage()).isEqualTo("java");
+ assertThat(profile.getName()).isEqualTo("Sonar way");
+ }
+
+ @Test
+ public void support_rule_templates() {
+ QProfile qProfile = new QProfile().setKey("java-sw").setName("Sonar way").setLanguage("java");
+ when(qProfiles.findAll()).thenReturn(Arrays.asList(qProfile));
+ ActiveRulesBuilder activeRulesBuilder = new ActiveRulesBuilder();
+ activeRulesBuilder.create(RuleKey.of("java", "S001")).setTemplateRuleKey("T001").setLanguage("java").activate();
+
+ RulesProfile profile = provider.provide(qProfiles, activeRulesBuilder.build(), settings);
+
+ assertThat(profile.getActiveRule("java", "S001").getRule().getTemplate().getKey()).isEqualTo("T001");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProviderTest.java
new file mode 100644
index 00000000000..2df29eaa61a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/RulesProviderTest.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import static org.mockito.Matchers.any;
+
+import org.apache.commons.lang.mutable.MutableBoolean;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.batch.rule.Rules;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.mock;
+import org.sonarqube.ws.Rules.ListResponse.Rule;
+import org.junit.Test;
+
+public class RulesProviderTest {
+ @Test
+ public void testRuleTranslation() {
+ RulesLoader loader = mock(RulesLoader.class);
+ when(loader.load(any(MutableBoolean.class))).thenReturn(Lists.newArrayList(getTestRule()));
+
+ RulesProvider provider = new RulesProvider();
+
+ Rules rules = provider.provide(loader);
+
+ assertThat(rules.findAll()).hasSize(1);
+ assertRule(rules.findAll().iterator().next());
+ }
+
+ private static void assertRule(org.sonar.api.batch.rule.Rule r) {
+ Rule testRule = getTestRule();
+
+ assertThat(r.name()).isEqualTo(testRule.getName());
+ assertThat(r.internalKey()).isEqualTo(testRule.getInternalKey());
+ assertThat(r.key().rule()).isEqualTo(testRule.getKey());
+ assertThat(r.key().repository()).isEqualTo(testRule.getRepository());
+ }
+
+ private static Rule getTestRule() {
+ Rule.Builder ruleBuilder = Rule.newBuilder();
+ ruleBuilder.setKey("key1");
+ ruleBuilder.setRepository("repo1");
+ ruleBuilder.setName("name");
+ ruleBuilder.setInternalKey("key1");
+ return ruleBuilder.build();
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/UsedQProfilesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/UsedQProfilesTest.java
new file mode 100644
index 00000000000..dd46501e68e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/rule/UsedQProfilesTest.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.rule;
+
+import org.junit.Test;
+import org.sonar.core.util.UtcDateUtils;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UsedQProfilesTest {
+
+ static final String JAVA_JSON = "{\"key\":\"p1\",\"language\":\"java\",\"name\":\"Sonar Way\",\"rulesUpdatedAt\":\"2014-01-15T00:00:00+0000\"}";
+ static final String PHP_JSON = "{\"key\":\"p2\",\"language\":\"php\",\"name\":\"Sonar Way\",\"rulesUpdatedAt\":\"2014-02-20T00:00:00+0000\"}";
+
+ @Test
+ public void from_and_to_json() {
+ QProfile java = new QProfile().setKey("p1").setName("Sonar Way").setLanguage("java")
+ .setRulesUpdatedAt(UtcDateUtils.parseDateTime("2014-01-15T00:00:00+0000"));
+ QProfile php = new QProfile().setKey("p2").setName("Sonar Way").setLanguage("php")
+ .setRulesUpdatedAt(UtcDateUtils.parseDateTime("2014-02-20T00:00:00+0000"));
+
+ UsedQProfiles used = new UsedQProfiles().add(java).add(php);
+ String json = "[" + JAVA_JSON + "," + PHP_JSON + "]";
+ assertThat(used.toJson()).isEqualTo(json);
+
+ used = UsedQProfiles.fromJson(json);
+ assertThat(used.profiles()).hasSize(2);
+ assertThat(used.profiles().first().getKey()).isEqualTo("p1");
+ assertThat(used.profiles().last().getKey()).isEqualTo("p2");
+ }
+
+ @Test
+ public void do_not_duplicate_profiles() {
+ QProfile java = new QProfile().setKey("p1").setName("Sonar Way").setLanguage("java");
+ QProfile php = new QProfile().setKey("p2").setName("Sonar Way").setLanguage("php");
+
+ UsedQProfiles used = new UsedQProfiles().addAll(Arrays.asList(java, java, php));
+ assertThat(used.profiles()).hasSize(2);
+ }
+
+ @Test
+ public void group_profiles_by_key() {
+ QProfile java = new QProfile().setKey("p1").setName("Sonar Way").setLanguage("java");
+ QProfile php = new QProfile().setKey("p2").setName("Sonar Way").setLanguage("php");
+
+ UsedQProfiles used = new UsedQProfiles().addAll(Arrays.asList(java, java, php));
+ Map<String, QProfile> map = used.profilesByKey();
+ assertThat(map).hasSize(2);
+ assertThat(map.get("p1")).isSameAs(java);
+ assertThat(map.get("p2")).isSameAs(php);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/LanguageVerifierTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/LanguageVerifierTest.java
new file mode 100644
index 00000000000..22a4f21091f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/LanguageVerifierTest.java
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+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.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Java;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.repository.language.DefaultLanguagesRepository;
+import org.sonar.batch.repository.language.LanguagesRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LanguageVerifierTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private Settings settings = new Settings();
+ private LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(Java.INSTANCE));
+ private DefaultFileSystem fs;
+
+ @Before
+ public void prepare() throws Exception {
+ fs = new DefaultFileSystem(temp.newFolder().toPath());
+ }
+
+ @Test
+ public void language_is_not_set() {
+ LanguageVerifier verifier = new LanguageVerifier(settings, languages, fs);
+ verifier.start();
+
+ // no failure and no language is forced
+ assertThat(fs.languages()).isEmpty();
+
+ verifier.stop();
+ }
+
+ @Test
+ public void language_is_empty() {
+ settings.setProperty("sonar.language", "");
+ LanguageVerifier verifier = new LanguageVerifier(settings, languages, fs);
+ verifier.start();
+
+ // no failure and no language is forced
+ assertThat(fs.languages()).isEmpty();
+
+ verifier.stop();
+ }
+
+ @Test
+ public void language_is_valid() {
+ settings.setProperty("sonar.language", "java");
+
+ LanguageVerifier verifier = new LanguageVerifier(settings, languages, fs);
+ verifier.start();
+
+ // no failure and language is hardly registered
+ assertThat(fs.languages()).contains("java");
+
+ verifier.stop();
+ }
+
+ @Test
+ public void language_is_not_valid() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("You must install a plugin that supports the language 'php'");
+
+ settings.setProperty("sonar.language", "php");
+ LanguageVerifier verifier = new LanguageVerifier(settings, languages, fs);
+ verifier.start();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java
new file mode 100644
index 00000000000..88fb18f06a4
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java
@@ -0,0 +1,162 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.GlobalSettings;
+import org.sonar.batch.report.AnalysisContextReportPublisher;
+import org.sonar.batch.repository.FileData;
+import org.sonar.batch.repository.ProjectRepositories;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ModuleSettingsTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private DefaultAnalysisMode mode;
+
+ @Before
+ public void before() {
+ mode = mock(DefaultAnalysisMode.class);
+ }
+
+ private ProjectRepositories createSettings(String module, Map<String, String> settingsMap) {
+ Table<String, String, FileData> fileData = ImmutableTable.of();
+ Table<String, String, String> settings = HashBasedTable.create();
+
+ for (Map.Entry<String, String> e : settingsMap.entrySet()) {
+ settings.put(module, e.getKey(), e.getValue());
+ }
+ return new ProjectRepositories(settings, fileData, null);
+ }
+
+ @Test
+ public void testOrderedProjects() {
+ ProjectDefinition grandParent = ProjectDefinition.create();
+ ProjectDefinition parent = ProjectDefinition.create();
+ ProjectDefinition child = ProjectDefinition.create();
+ grandParent.addSubProject(parent);
+ parent.addSubProject(child);
+
+ List<ProjectDefinition> hierarchy = ModuleSettings.getTopDownParentProjects(child);
+ assertThat(hierarchy.get(0)).isEqualTo(grandParent);
+ assertThat(hierarchy.get(1)).isEqualTo(parent);
+ assertThat(hierarchy.get(2)).isEqualTo(child);
+ }
+
+ @Test
+ public void test_loading_of_module_settings() {
+ GlobalSettings globalSettings = mock(GlobalSettings.class);
+ when(globalSettings.getDefinitions()).thenReturn(new PropertyDefinitions());
+ when(globalSettings.getProperties()).thenReturn(ImmutableMap.of(
+ "overridding", "batch",
+ "on-batch", "true"));
+
+ ProjectRepositories projRepos = createSettings("struts-core", ImmutableMap.of("on-module", "true", "overridding", "module"));
+
+ ProjectDefinition module = ProjectDefinition.create().setKey("struts-core");
+
+ ModuleSettings moduleSettings = new ModuleSettings(globalSettings, module, projRepos, mode, mock(AnalysisContextReportPublisher.class));
+
+ assertThat(moduleSettings.getString("overridding")).isEqualTo("module");
+ assertThat(moduleSettings.getString("on-batch")).isEqualTo("true");
+ assertThat(moduleSettings.getString("on-module")).isEqualTo("true");
+
+ }
+
+ // SONAR-6386
+ @Test
+ public void test_loading_of_parent_module_settings_for_new_module() {
+ GlobalSettings globalSettings = mock(GlobalSettings.class);
+ when(globalSettings.getDefinitions()).thenReturn(new PropertyDefinitions());
+ when(globalSettings.getProperties()).thenReturn(ImmutableMap.of(
+ "overridding", "batch",
+ "on-batch", "true"));
+
+ ProjectRepositories projRepos = createSettings("struts", ImmutableMap.of("on-module", "true", "overridding", "module"));
+
+ ProjectDefinition module = ProjectDefinition.create().setKey("struts-core");
+ ProjectDefinition.create().setKey("struts").addSubProject(module);
+
+ ModuleSettings moduleSettings = new ModuleSettings(globalSettings, module, projRepos, mode, mock(AnalysisContextReportPublisher.class));
+
+ assertThat(moduleSettings.getString("overridding")).isEqualTo("module");
+ assertThat(moduleSettings.getString("on-batch")).isEqualTo("true");
+ assertThat(moduleSettings.getString("on-module")).isEqualTo("true");
+ }
+
+ @Test
+ public void should_not_fail_when_accessing_secured_properties() {
+ GlobalSettings batchSettings = mock(GlobalSettings.class);
+ when(batchSettings.getDefinitions()).thenReturn(new PropertyDefinitions());
+ when(batchSettings.getProperties()).thenReturn(ImmutableMap.of(
+ "sonar.foo.secured", "bar"));
+
+ ProjectRepositories projSettingsRepo = createSettings("struts-core", ImmutableMap.of("sonar.foo.license.secured", "bar2"));
+
+ ProjectDefinition module = ProjectDefinition.create().setKey("struts-core");
+
+ ModuleSettings moduleSettings = new ModuleSettings(batchSettings, module, projSettingsRepo, mode, mock(AnalysisContextReportPublisher.class));
+
+ assertThat(moduleSettings.getString("sonar.foo.license.secured")).isEqualTo("bar2");
+ assertThat(moduleSettings.getString("sonar.foo.secured")).isEqualTo("bar");
+ }
+
+ @Test
+ public void should_fail_when_accessing_secured_properties_in_issues() {
+ GlobalSettings batchSettings = mock(GlobalSettings.class);
+ when(batchSettings.getDefinitions()).thenReturn(new PropertyDefinitions());
+ when(batchSettings.getProperties()).thenReturn(ImmutableMap.of(
+ "sonar.foo.secured", "bar"));
+
+ ProjectRepositories projSettingsRepo = createSettings("struts-core", ImmutableMap.of("sonar.foo.license.secured", "bar2"));
+
+ when(mode.isIssues()).thenReturn(true);
+
+ ProjectDefinition module = ProjectDefinition.create().setKey("struts-core");
+
+ ModuleSettings moduleSettings = new ModuleSettings(batchSettings, module, projSettingsRepo, mode, mock(AnalysisContextReportPublisher.class));
+
+ assertThat(moduleSettings.getString("sonar.foo.license.secured")).isEqualTo("bar2");
+
+ thrown.expect(MessageException.class);
+ thrown
+ .expectMessage(
+ "Access to the secured property 'sonar.foo.secured' is not possible in issues mode. The SonarQube plugin which requires this property must be deactivated in issues mode.");
+ moduleSettings.getString("sonar.foo.secured");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectExclusionsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectExclusionsTest.java
new file mode 100644
index 00000000000..4f9b403a94e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectExclusionsTest.java
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.junit.Test;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.config.Settings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectExclusionsTest {
+
+ ProjectReactor newReactor(String rootKey, String... moduleKeys) {
+ ProjectDefinition root = ProjectDefinition.create().setKey(rootKey);
+ for (String moduleKey : moduleKeys) {
+ ProjectDefinition module = ProjectDefinition.create().setKey(moduleKey);
+ root.addSubProject(module);
+ }
+ return new ProjectReactor(root);
+ }
+
+ @Test
+ public void testSkippedModules() {
+ Settings settings = new Settings();
+ settings.setProperty("sonar.skippedModules", "sub1,sub3");
+
+ ProjectReactor reactor = newReactor("root", "sub1", "sub2");
+
+ ProjectExclusions exclusions = new ProjectExclusions(settings);
+ exclusions.apply(reactor);
+
+ assertThat(reactor.getProject("root")).isNotNull();
+ assertThat(reactor.getProject("sub1")).isNull();
+ assertThat(reactor.getProject("sub2")).isNotNull();
+ }
+
+ @Test
+ public void testNoSkippedModules() {
+ Settings settings = new Settings();
+ ProjectReactor reactor = newReactor("root", "sub1", "sub2");
+ ProjectExclusions exclusions = new ProjectExclusions(settings);
+ exclusions.apply(reactor);
+
+ assertThat(reactor.getProject("root")).isNotNull();
+ assertThat(reactor.getProject("sub1")).isNotNull();
+ assertThat(reactor.getProject("sub2")).isNotNull();
+ }
+
+ @Test
+ public void testIncludedModules() {
+ Settings settings = new Settings();
+ settings.setProperty("sonar.includedModules", "sub1");
+ ProjectReactor reactor = newReactor("root", "sub1", "sub2");
+ ProjectExclusions exclusions = new ProjectExclusions(settings);
+ exclusions.apply(reactor);
+
+ assertThat(reactor.getProject("root")).isNotNull();
+ assertThat(reactor.getProject("sub1")).isNotNull();
+ assertThat(reactor.getProject("sub2")).isNull();
+ }
+
+ @Test
+ public void shouldBeExcludedIfParentIsExcluded() {
+ ProjectDefinition sub11 = ProjectDefinition.create().setKey("sub11");
+ ProjectDefinition sub1 = ProjectDefinition.create().setKey("sub1").addSubProject(sub11);
+ ProjectDefinition root = ProjectDefinition.create().setKey("root").addSubProject(sub1);
+
+ Settings settings = new Settings();
+ settings.setProperty("sonar.skippedModules", "sub1");
+
+ ProjectReactor reactor = new ProjectReactor(root);
+ ProjectExclusions exclusions = new ProjectExclusions(settings);
+ exclusions.apply(reactor);
+
+ assertThat(reactor.getProject("root")).isNotNull();
+ assertThat(reactor.getProject("sub1")).isNull();
+ assertThat(reactor.getProject("sub11")).isNull();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldFailIfExcludingRoot() {
+ Settings settings = new Settings();
+ settings.setProperty("sonar.skippedModules", "sub1,root");
+
+ ProjectReactor reactor = newReactor("root", "sub1", "sub2");
+ ProjectExclusions exclusions = new ProjectExclusions(settings);
+ exclusions.apply(reactor);
+ }
+
+ @Test
+ public void shouldIgnoreMavenGroupId() {
+ ProjectReactor reactor = newReactor("org.apache.struts:struts", "org.apache.struts:struts-core", "org.apache.struts:struts-taglib");
+
+ Settings settings = new Settings();
+ settings.setProperty("sonar.skippedModules", "struts-taglib");
+
+ ProjectExclusions exclusions = new ProjectExclusions(settings);
+ exclusions.apply(reactor);
+
+ assertThat(reactor.getProject("org.apache.struts:struts")).isNotNull();
+ assertThat(reactor.getProject("org.apache.struts:struts-core")).isNotNull();
+ assertThat(reactor.getProject("org.apache.struts:struts-taglib")).isNull();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectLockTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectLockTest.java
new file mode 100644
index 00000000000..c03240b7341
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectLockTest.java
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.home.cache.DirectoryLock;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+public class ProjectLockTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+ private ProjectLock lock;
+
+ @Before
+ public void setUp() {
+ lock = setUpTest(tempFolder.getRoot());
+ }
+
+ private ProjectLock setUpTest(File file) {
+ ProjectReactor projectReactor = mock(ProjectReactor.class);
+ ProjectDefinition projectDefinition = mock(ProjectDefinition.class);
+ when(projectReactor.getRoot()).thenReturn(projectDefinition);
+ when(projectDefinition.getWorkDir()).thenReturn(file);
+
+ return new ProjectLock(projectReactor);
+ }
+
+ @Test
+ public void tryLock() {
+ Path lockFilePath = tempFolder.getRoot().toPath().resolve(DirectoryLock.LOCK_FILE_NAME);
+ lock.tryLock();
+ assertThat(Files.exists(lockFilePath)).isTrue();
+ assertThat(Files.isRegularFile(lockFilePath)).isTrue();
+
+ lock.stop();
+ assertThat(Files.exists(lockFilePath)).isTrue();
+ }
+
+ @Test
+ public void tryLockConcurrently() {
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Another SonarQube analysis is already in progress for this project");
+ lock.tryLock();
+ lock.tryLock();
+ }
+
+ @Test
+ /**
+ * If there is an error starting up the scan, we'll still try to unlock even if the lock
+ * was never done
+ */
+ public void stopWithoutStarting() {
+ lock.stop();
+ lock.stop();
+ }
+
+ @Test
+ public void tryLockTwice() {
+ lock.tryLock();
+ lock.stop();
+ lock.tryLock();
+ lock.stop();
+ }
+
+ @Test
+ public void unLockWithNoLock() {
+ lock.stop();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorBuilderTest.java
new file mode 100644
index 00000000000..d1a61853ef2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorBuilderTest.java
@@ -0,0 +1,683 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import com.google.common.collect.Maps;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.batch.analysis.AnalysisProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProjectReactorBuilderTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private AnalysisMode mode;
+
+ @Before
+ public void setUp() {
+ mode = mock(AnalysisMode.class);
+ }
+
+ @Test
+ public void shouldDefineSimpleProject() {
+ ProjectDefinition projectDefinition = loadProjectDefinition("simple-project");
+
+ assertThat(projectDefinition.getKey()).isEqualTo("com.foo.project");
+ assertThat(projectDefinition.getName()).isEqualTo("Foo Project");
+ assertThat(projectDefinition.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ assertThat(projectDefinition.getDescription()).isEqualTo("Description of Foo Project");
+ assertThat(projectDefinition.getSourceDirs()).contains("sources");
+ }
+
+ @Test
+ public void shouldFailIfUnexistingSourceDirectory() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("The folder 'unexisting-source-dir' does not exist for 'com.foo.project' (base directory = "
+ + getResource(this.getClass(), "simple-project-with-unexisting-source-dir") + ")");
+
+ loadProjectDefinition("simple-project-with-unexisting-source-dir");
+ }
+
+ @Test
+ public void fail_if_sources_not_set() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("You must define the following mandatory properties for 'com.foo.project': sonar.sources");
+ loadProjectDefinition("simple-project-with-missing-source-dir");
+ }
+
+ @Test
+ public void shouldNotFailIfBlankSourceDirectory() {
+ loadProjectDefinition("simple-project-with-blank-source-dir");
+ }
+
+ @Test
+ public void modulesDuplicateIds() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("Two modules have the same id: 'module1'. Each module must have a unique id.");
+
+ loadProjectDefinition("multi-module-duplicate-id");
+ }
+
+ @Test
+ public void modulesRepeatedIds() {
+ ProjectDefinition rootProject = loadProjectDefinition("multi-module-repeated-id");
+
+ List<ProjectDefinition> modules = rootProject.getSubProjects();
+ assertThat(modules.size()).isEqualTo(1);
+ // Module 1
+ ProjectDefinition module1 = modules.get(0);
+ assertThat(module1.getKey()).isEqualTo("com.foo.project:module1");
+ assertThat(module1.getName()).isEqualTo("Foo Module 1");
+
+ // Module 1 -> Module 1
+ ProjectDefinition module1_module1 = module1.getSubProjects().get(0);
+ assertThat(module1_module1.getKey()).isEqualTo("com.foo.project:module1:module1");
+ assertThat(module1_module1.getName()).isEqualTo("Foo Sub Module 1");
+ }
+
+ @Test
+ public void shouldDefineMultiModuleProjectWithDefinitionsAllInRootProject() throws IOException {
+ ProjectDefinition rootProject = loadProjectDefinition("multi-module-definitions-all-in-root");
+
+ // CHECK ROOT
+ assertThat(rootProject.getKey()).isEqualTo("com.foo.project");
+ assertThat(rootProject.getName()).isEqualTo("Foo Project");
+ assertThat(rootProject.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ assertThat(rootProject.getDescription()).isEqualTo("Description of Foo Project");
+ // root project must not contain some properties - even if they are defined in the root properties file
+ assertThat(rootProject.getSourceDirs().contains("sources")).isFalse();
+ assertThat(rootProject.getTestDirs().contains("tests")).isFalse();
+ // and module properties must have been cleaned
+ assertThat(rootProject.properties().get("module1.sonar.projectKey")).isNull();
+ assertThat(rootProject.properties().get("module2.sonar.projectKey")).isNull();
+ // Check baseDir and workDir
+ assertThat(rootProject.getBaseDir().getCanonicalFile())
+ .isEqualTo(getResource(this.getClass(), "multi-module-definitions-all-in-root"));
+ assertThat(rootProject.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-module-definitions-all-in-root"), ".sonar"));
+
+ // CHECK MODULES
+ List<ProjectDefinition> modules = rootProject.getSubProjects();
+ assertThat(modules.size()).isEqualTo(2);
+
+ // Module 1
+ ProjectDefinition module1 = modules.get(0);
+ assertThat(module1.getBaseDir().getCanonicalFile()).isEqualTo(getResource(this.getClass(), "multi-module-definitions-all-in-root/module1"));
+ assertThat(module1.getKey()).isEqualTo("com.foo.project:module1");
+ assertThat(module1.getName()).isEqualTo("module1");
+ assertThat(module1.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ // Description should not be inherited from parent if not set
+ assertThat(module1.getDescription()).isNull();
+ assertThat(module1.getSourceDirs()).contains("sources");
+ assertThat(module1.getTestDirs()).contains("tests");
+ assertThat(module1.getBinaries()).contains("target/classes");
+ // and module properties must have been cleaned
+ assertThat(module1.properties().get("module1.sonar.projectKey")).isNull();
+ assertThat(module1.properties().get("module2.sonar.projectKey")).isNull();
+ // Check baseDir and workDir
+ assertThat(module1.getBaseDir().getCanonicalFile())
+ .isEqualTo(getResource(this.getClass(), "multi-module-definitions-all-in-root/module1"));
+ assertThat(module1.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-module-definitions-all-in-root"), ".sonar/com.foo.project_module1"));
+
+ // Module 2
+ ProjectDefinition module2 = modules.get(1);
+ assertThat(module2.getBaseDir().getCanonicalFile()).isEqualTo(getResource(this.getClass(), "multi-module-definitions-all-in-root/module2"));
+ assertThat(module2.getKey()).isEqualTo("com.foo.project:com.foo.project.module2");
+ assertThat(module2.getName()).isEqualTo("Foo Module 2");
+ assertThat(module2.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ assertThat(module2.getDescription()).isEqualTo("Description of Module 2");
+ assertThat(module2.getSourceDirs()).contains("src");
+ assertThat(module2.getTestDirs()).contains("tests");
+ assertThat(module2.getBinaries()).contains("target/classes");
+ // and module properties must have been cleaned
+ assertThat(module2.properties().get("module1.sonar.projectKey")).isNull();
+ assertThat(module2.properties().get("module2.sonar.projectKey")).isNull();
+ // Check baseDir and workDir
+ assertThat(module2.getBaseDir().getCanonicalFile())
+ .isEqualTo(getResource(this.getClass(), "multi-module-definitions-all-in-root/module2"));
+ assertThat(module2.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-module-definitions-all-in-root"), ".sonar/com.foo.project_com.foo.project.module2"));
+ }
+
+ // SONAR-4876
+ @Test
+ public void shouldDefineMultiModuleProjectWithModuleKey() {
+ ProjectDefinition rootProject = loadProjectDefinition("multi-module-definitions-moduleKey");
+
+ // CHECK ROOT
+ // module properties must have been cleaned
+ assertThat(rootProject.properties().get("module1.sonar.moduleKey")).isNull();
+ assertThat(rootProject.properties().get("module2.sonar.moduleKey")).isNull();
+
+ // CHECK MODULES
+ List<ProjectDefinition> modules = rootProject.getSubProjects();
+ assertThat(modules.size()).isEqualTo(2);
+
+ // Module 2
+ ProjectDefinition module2 = modules.get(1);
+ assertThat(module2.getKey()).isEqualTo("com.foo.project.module2");
+ }
+
+ // SONARPLUGINS-2421
+ @Test
+ public void shouldDefineMultiLanguageProjectWithDefinitionsAllInRootProject() throws IOException {
+ ProjectDefinition rootProject = loadProjectDefinition("multi-language-definitions-all-in-root");
+
+ // CHECK ROOT
+ assertThat(rootProject.getKey()).isEqualTo("example");
+ assertThat(rootProject.getName()).isEqualTo("Example");
+ assertThat(rootProject.getVersion()).isEqualTo("1.0");
+
+ // CHECK MODULES
+ List<ProjectDefinition> modules = rootProject.getSubProjects();
+ assertThat(modules.size()).isEqualTo(2);
+
+ // Module 1
+ ProjectDefinition module1 = modules.get(0);
+ assertThat(module1.getBaseDir().getCanonicalFile()).isEqualTo(getResource(this.getClass(), "multi-language-definitions-all-in-root"));
+ assertThat(module1.getSourceDirs()).contains("src/main/java");
+ // and module properties must have been cleaned
+ assertThat(module1.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-language-definitions-all-in-root"), ".sonar/example_java-module"));
+
+ // Module 2
+ ProjectDefinition module2 = modules.get(1);
+ assertThat(module2.getBaseDir().getCanonicalFile()).isEqualTo(getResource(this.getClass(), "multi-language-definitions-all-in-root"));
+ assertThat(module2.getSourceDirs()).contains("src/main/groovy");
+ // and module properties must have been cleaned
+ assertThat(module2.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-language-definitions-all-in-root"), ".sonar/example_groovy-module"));
+ }
+
+ @Test
+ public void shouldDefineMultiModuleProjectWithBaseDir() {
+ ProjectDefinition rootProject = loadProjectDefinition("multi-module-with-basedir");
+ List<ProjectDefinition> modules = rootProject.getSubProjects();
+ assertThat(modules.size()).isEqualTo(1);
+ assertThat(modules.get(0).getKey()).isEqualTo("com.foo.project:com.foo.project.module1");
+ }
+
+ @Test
+ public void shouldFailIfUnexistingModuleBaseDir() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("The base directory of the module 'module1' does not exist: "
+ + getResource(this.getClass(), "multi-module-with-unexisting-basedir").getAbsolutePath() + File.separator + "module1");
+
+ loadProjectDefinition("multi-module-with-unexisting-basedir");
+ }
+
+ @Test
+ public void shouldFailIfUnexistingSourceFolderInheritedInMultimodule() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("The folder 'unexisting-source-dir' does not exist for 'com.foo.project:module1' (base directory = "
+ + getResource(this.getClass(), "multi-module-with-unexisting-source-dir").getAbsolutePath() + File.separator + "module1)");
+
+ loadProjectDefinition("multi-module-with-unexisting-source-dir");
+ }
+
+ @Test
+ public void shouldFailIfExplicitUnexistingTestFolder() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("The folder 'tests' does not exist for 'com.foo.project' (base directory = "
+ + getResource(this.getClass(), "simple-project-with-unexisting-test-dir").getAbsolutePath());
+
+ loadProjectDefinition("simple-project-with-unexisting-test-dir");
+ }
+
+ @Test
+ public void shouldFailIfExplicitUnexistingTestFolderOnModule() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("The folder 'tests' does not exist for 'module1' (base directory = "
+ + getResource(this.getClass(), "multi-module-with-explicit-unexisting-test-dir").getAbsolutePath() + File.separator + "module1)");
+
+ loadProjectDefinition("multi-module-with-explicit-unexisting-test-dir");
+ }
+
+ @Test
+ public void multiModuleProperties() {
+ ProjectDefinition projectDefinition = loadProjectDefinition("big-multi-module-definitions-all-in-root");
+
+ assertThat(projectDefinition.properties().get("module11.property")).isNull();
+ assertThat(projectDefinition.properties().get("sonar.profile")).isEqualTo("Foo");
+ ProjectDefinition module1 = null;
+ ProjectDefinition module2 = null;
+ for (ProjectDefinition prj : projectDefinition.getSubProjects()) {
+ if (prj.getKey().equals("com.foo.project:module1")) {
+ module1 = prj;
+ } else if (prj.getKey().equals("com.foo.project:module2")) {
+ module2 = prj;
+ }
+ }
+ assertThat(module1.properties().get("module11.property")).isNull();
+ assertThat(module1.properties().get("property")).isNull();
+ assertThat(module1.properties().get("sonar.profile")).isEqualTo("Foo");
+ assertThat(module2.properties().get("module11.property")).isNull();
+ assertThat(module2.properties().get("property")).isNull();
+ assertThat(module2.properties().get("sonar.profile")).isEqualTo("Foo");
+
+ ProjectDefinition module11 = null;
+ ProjectDefinition module12 = null;
+ for (ProjectDefinition prj : module1.getSubProjects()) {
+ if (prj.getKey().equals("com.foo.project:module1:module11")) {
+ module11 = prj;
+ } else if (prj.getKey().equals("com.foo.project:module1:module12")) {
+ module12 = prj;
+ }
+ }
+ assertThat(module11.properties().get("module1.module11.property")).isNull();
+ assertThat(module11.properties().get("module11.property")).isNull();
+ assertThat(module11.properties().get("property")).isEqualTo("My module11 property");
+ assertThat(module11.properties().get("sonar.profile")).isEqualTo("Foo");
+ assertThat(module12.properties().get("module11.property")).isNull();
+ assertThat(module12.properties().get("property")).isNull();
+ assertThat(module12.properties().get("sonar.profile")).isEqualTo("Foo");
+ }
+
+ @Test
+ public void shouldRemoveModulePropertiesFromTaskProperties() {
+ Map<String, String> props = loadProps("big-multi-module-definitions-all-in-root");
+
+ AnalysisProperties taskProperties = new AnalysisProperties(props, null);
+ assertThat(taskProperties.property("module1.module11.property")).isEqualTo("My module11 property");
+
+ new ProjectReactorBuilder(taskProperties, mode).execute();
+
+ assertThat(taskProperties.property("module1.module11.property")).isNull();
+ }
+
+ @Test
+ public void shouldFailIfMandatoryPropertiesAreNotPresent() {
+ Map<String, String> props = new HashMap<>();
+ props.put("foo1", "bla");
+ props.put("foo4", "bla");
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("You must define the following mandatory properties for 'Unknown': foo2, foo3");
+
+ ProjectReactorBuilder.checkMandatoryProperties(props, new String[] {"foo1", "foo2", "foo3"});
+ }
+
+ @Test
+ public void shouldFailIfMandatoryPropertiesAreNotPresentButWithProjectKey() {
+ Map<String, String> props = new HashMap<>();
+ props.put("foo1", "bla");
+ props.put("sonar.projectKey", "my-project");
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("You must define the following mandatory properties for 'my-project': foo2, foo3");
+
+ ProjectReactorBuilder.checkMandatoryProperties(props, new String[] {"foo1", "foo2", "foo3"});
+ }
+
+ @Test
+ public void shouldNotFailIfMandatoryPropertiesArePresent() {
+ Map<String, String> props = new HashMap<>();
+ props.put("foo1", "bla");
+ props.put("foo4", "bla");
+
+ ProjectReactorBuilder.checkMandatoryProperties(props, new String[] {"foo1"});
+
+ // No exception should be thrown
+ }
+
+ @Test
+ public void shouldGetRelativeFile() {
+ assertThat(ProjectReactorBuilder.resolvePath(getResource(this.getClass(), "/"), "shouldGetFile/foo.properties"))
+ .isEqualTo(getResource(this.getClass(), "shouldGetFile/foo.properties"));
+ }
+
+ @Test
+ public void shouldGetAbsoluteFile() {
+ File file = getResource(this.getClass(), "shouldGetFile/foo.properties");
+
+ assertThat(ProjectReactorBuilder.resolvePath(getResource(this.getClass(), "/"), file.getAbsolutePath()))
+ .isEqualTo(file);
+ }
+
+ @Test
+ public void shouldMergeParentProperties() {
+ // Use a random value to avoid VM optimization that would create constant String and make s1 and s2 the same object
+ int i = (int) Math.random() * 10;
+ String s1 = "value" + i;
+ String s2 = "value" + i;
+ Map<String, String> parentProps = new HashMap<>();
+ parentProps.put("toBeMergeProps", "fooParent");
+ parentProps.put("existingChildProp", "barParent");
+ parentProps.put("duplicatedProp", s1);
+ parentProps.put("sonar.projectDescription", "Desc from Parent");
+
+ Map<String, String> childProps = new HashMap<>();
+ childProps.put("existingChildProp", "barChild");
+ childProps.put("otherProp", "tutuChild");
+ childProps.put("duplicatedProp", s2);
+
+ ProjectReactorBuilder.mergeParentProperties(childProps, parentProps);
+
+ assertThat(childProps).hasSize(4);
+ assertThat(childProps.get("toBeMergeProps")).isEqualTo("fooParent");
+ assertThat(childProps.get("existingChildProp")).isEqualTo("barChild");
+ assertThat(childProps.get("otherProp")).isEqualTo("tutuChild");
+ assertThat(childProps.get("sonar.projectDescription")).isNull();
+ assertThat(childProps.get("duplicatedProp")).isSameAs(parentProps.get("duplicatedProp"));
+ }
+
+ @Test
+ public void shouldInitRootWorkDir() {
+ ProjectReactorBuilder builder = new ProjectReactorBuilder(new AnalysisProperties(Maps.<String, String>newHashMap(), null), mode);
+ File baseDir = new File("target/tmp/baseDir");
+
+ File workDir = builder.initRootProjectWorkDir(baseDir, Maps.<String, String>newHashMap());
+
+ assertThat(workDir).isEqualTo(new File(baseDir, ".sonar"));
+ }
+
+ @Test
+ public void nonAssociatedMode() {
+ when(mode.isIssues()).thenReturn(true);
+ ProjectDefinition project = loadProjectDefinition("multi-module-with-basedir-not-associated");
+
+ assertThat(project.getKey()).isEqualTo("project");
+ }
+
+ @Test
+ public void shouldInitWorkDirWithCustomRelativeFolder() {
+ Map<String, String> props = Maps.<String, String>newHashMap();
+ props.put("sonar.working.directory", ".foo");
+ ProjectReactorBuilder builder = new ProjectReactorBuilder(new AnalysisProperties(props, null), mode);
+ File baseDir = new File("target/tmp/baseDir");
+
+ File workDir = builder.initRootProjectWorkDir(baseDir, props);
+
+ assertThat(workDir).isEqualTo(new File(baseDir, ".foo"));
+ }
+
+ @Test
+ public void shouldInitRootWorkDirWithCustomAbsoluteFolder() {
+ Map<String, String> props = Maps.<String, String>newHashMap();
+ props.put("sonar.working.directory", new File("src").getAbsolutePath());
+ ProjectReactorBuilder builder = new ProjectReactorBuilder(new AnalysisProperties(props, null), mode);
+ File baseDir = new File("target/tmp/baseDir");
+
+ File workDir = builder.initRootProjectWorkDir(baseDir, props);
+
+ assertThat(workDir).isEqualTo(new File("src").getAbsoluteFile());
+ }
+
+ @Test
+ public void shouldFailIf2ModulesWithSameKey() {
+ Map<String, String> props = new HashMap<>();
+ props.put("sonar.projectKey", "root");
+ ProjectDefinition root = ProjectDefinition.create().setProperties(props);
+
+ Map<String, String> props1 = new HashMap<>();
+ props1.put("sonar.projectKey", "mod1");
+ root.addSubProject(ProjectDefinition.create().setProperties(props1));
+
+ // Check uniqueness of a new module: OK
+ Map<String, String> props2 = new HashMap<>();
+ props2.put("sonar.projectKey", "mod2");
+ ProjectDefinition mod2 = ProjectDefinition.create().setProperties(props2);
+ ProjectReactorBuilder.checkUniquenessOfChildKey(mod2, root);
+
+ // Now, add it and check again
+ root.addSubProject(mod2);
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("Project 'root' can't have 2 modules with the following key: mod2");
+
+ ProjectReactorBuilder.checkUniquenessOfChildKey(mod2, root);
+ }
+
+ @Test
+ public void shouldSetModuleKeyIfNotPresent() {
+ Map<String, String> props = new HashMap<>();
+ props.put("sonar.projectVersion", "1.0");
+
+ // should be set
+ ProjectReactorBuilder.setModuleKeyAndNameIfNotDefined(props, "foo", "parent");
+ assertThat(props.get("sonar.moduleKey")).isEqualTo("parent:foo");
+ assertThat(props.get("sonar.projectName")).isEqualTo("foo");
+
+ // but not this 2nd time
+ ProjectReactorBuilder.setModuleKeyAndNameIfNotDefined(props, "bar", "parent");
+ assertThat(props.get("sonar.moduleKey")).isEqualTo("parent:foo");
+ assertThat(props.get("sonar.projectName")).isEqualTo("foo");
+ }
+
+ private ProjectDefinition loadProjectDefinition(String projectFolder) {
+ Map<String, String> props = loadProps(projectFolder);
+ AnalysisProperties bootstrapProps = new AnalysisProperties(props, null);
+ ProjectReactor projectReactor = new ProjectReactorBuilder(bootstrapProps, mode).execute();
+ return projectReactor.getRoot();
+ }
+
+ protected static Properties toProperties(File propertyFile) {
+ Properties propsFromFile = new Properties();
+ try (FileInputStream fileInputStream = new FileInputStream(propertyFile)) {
+ propsFromFile.load(fileInputStream);
+ } catch (IOException e) {
+ throw new IllegalStateException("Impossible to read the property file: " + propertyFile.getAbsolutePath(), e);
+ }
+ // Trim properties
+ for (String propKey : propsFromFile.stringPropertyNames()) {
+ propsFromFile.setProperty(propKey, StringUtils.trim(propsFromFile.getProperty(propKey)));
+ }
+ return propsFromFile;
+ }
+
+ private Map<String, String> loadProps(String projectFolder) {
+ Map<String, String> props = Maps.<String, String>newHashMap();
+ Properties runnerProps = toProperties(getResource(this.getClass(), projectFolder + "/sonar-project.properties"));
+ for (final String name : runnerProps.stringPropertyNames()) {
+ props.put(name, runnerProps.getProperty(name));
+ }
+ props.put("sonar.projectBaseDir", getResource(this.getClass(), projectFolder).getAbsolutePath());
+ return props;
+ }
+
+ public Map<String, String> toMap(Properties props) {
+ Map<String, String> result = new HashMap<>();
+ for (Map.Entry<Object, Object> entry : props.entrySet()) {
+ result.put(entry.getKey().toString(), entry.getValue().toString());
+ }
+ return result;
+ }
+
+ @Test
+ public void shouldGetList() {
+ Map<String, String> props = new HashMap<>();
+
+ props.put("prop", " foo ,, bar , \n\ntoto,tutu");
+ assertThat(ProjectReactorBuilder.getListFromProperty(props, "prop")).containsOnly("foo", "bar", "toto", "tutu");
+ }
+
+ @Test
+ public void shouldGetEmptyList() {
+ Map<String, String> props = new HashMap<>();
+
+ props.put("prop", "");
+ assertThat(ProjectReactorBuilder.getListFromProperty(props, "prop")).isEmpty();
+ }
+
+ @Test
+ public void shouldGetListFromFile() throws IOException {
+ String filePath = "shouldGetList/foo.properties";
+ Map<String, String> props = loadPropsFromFile(filePath);
+
+ assertThat(ProjectReactorBuilder.getListFromProperty(props, "prop")).containsOnly("foo", "bar", "toto", "tutu");
+ }
+
+ @Test
+ public void shouldDefineProjectWithBuildDir() {
+ ProjectDefinition rootProject = loadProjectDefinition("simple-project-with-build-dir");
+ File buildDir = rootProject.getBuildDir();
+ assertThat(buildDir).isDirectory().exists();
+ assertThat(new File(buildDir, "report.txt")).isFile().exists();
+ assertThat(buildDir.getName()).isEqualTo("build");
+ }
+
+ @Test
+ public void doNotMixPropertiesWhenModuleKeyIsPrefixOfAnother() throws IOException {
+ ProjectDefinition rootProject = loadProjectDefinition("multi-module-definitions-same-prefix");
+
+ // CHECK ROOT
+ assertThat(rootProject.getKey()).isEqualTo("com.foo.project");
+ assertThat(rootProject.getName()).isEqualTo("Foo Project");
+ assertThat(rootProject.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ assertThat(rootProject.getDescription()).isEqualTo("Description of Foo Project");
+ // root project must not contain some properties - even if they are defined in the root properties file
+ assertThat(rootProject.getSourceDirs().contains("sources")).isFalse();
+ assertThat(rootProject.getTestDirs().contains("tests")).isFalse();
+ // and module properties must have been cleaned
+ assertThat(rootProject.properties().get("module1.sonar.projectKey")).isNull();
+ assertThat(rootProject.properties().get("module2.sonar.projectKey")).isNull();
+ // Check baseDir and workDir
+ assertThat(rootProject.getBaseDir().getCanonicalFile())
+ .isEqualTo(getResource(this.getClass(), "multi-module-definitions-same-prefix"));
+ assertThat(rootProject.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-module-definitions-same-prefix"), ".sonar"));
+
+ // CHECK MODULES
+ List<ProjectDefinition> modules = rootProject.getSubProjects();
+ assertThat(modules.size()).isEqualTo(2);
+
+ // Module 1
+ ProjectDefinition module1 = modules.get(0);
+ assertThat(module1.getBaseDir().getCanonicalFile()).isEqualTo(getResource(this.getClass(), "multi-module-definitions-same-prefix/module1"));
+ assertThat(module1.getKey()).isEqualTo("com.foo.project:module1");
+ assertThat(module1.getName()).isEqualTo("module1");
+ assertThat(module1.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ // Description should not be inherited from parent if not set
+ assertThat(module1.getDescription()).isNull();
+ assertThat(module1.getSourceDirs()).contains("sources");
+ assertThat(module1.getTestDirs()).contains("tests");
+ assertThat(module1.getBinaries()).contains("target/classes");
+ // and module properties must have been cleaned
+ assertThat(module1.properties().get("module1.sonar.projectKey")).isNull();
+ assertThat(module1.properties().get("module2.sonar.projectKey")).isNull();
+ // Check baseDir and workDir
+ assertThat(module1.getBaseDir().getCanonicalFile())
+ .isEqualTo(getResource(this.getClass(), "multi-module-definitions-same-prefix/module1"));
+ assertThat(module1.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-module-definitions-same-prefix"), ".sonar/com.foo.project_module1"));
+
+ // Module 1 Feature
+ ProjectDefinition module1Feature = modules.get(1);
+ assertThat(module1Feature.getBaseDir().getCanonicalFile()).isEqualTo(getResource(this.getClass(), "multi-module-definitions-same-prefix/module1.feature"));
+ assertThat(module1Feature.getKey()).isEqualTo("com.foo.project:com.foo.project.module1.feature");
+ assertThat(module1Feature.getName()).isEqualTo("Foo Module 1 Feature");
+ assertThat(module1Feature.getVersion()).isEqualTo("1.0-SNAPSHOT");
+ assertThat(module1Feature.getDescription()).isEqualTo("Description of Module 1 Feature");
+ assertThat(module1Feature.getSourceDirs()).contains("src");
+ assertThat(module1Feature.getTestDirs()).contains("tests");
+ assertThat(module1Feature.getBinaries()).contains("target/classes");
+ // and module properties must have been cleaned
+ assertThat(module1Feature.properties().get("module1.sonar.projectKey")).isNull();
+ assertThat(module1Feature.properties().get("module2.sonar.projectKey")).isNull();
+ // Check baseDir and workDir
+ assertThat(module1Feature.getBaseDir().getCanonicalFile())
+ .isEqualTo(getResource(this.getClass(), "multi-module-definitions-same-prefix/module1.feature"));
+ assertThat(module1Feature.getWorkDir().getCanonicalFile())
+ .isEqualTo(new File(getResource(this.getClass(), "multi-module-definitions-same-prefix"), ".sonar/com.foo.project_com.foo.project.module1.feature"));
+ }
+
+ @Test
+ public void should_log_a_warning_when_a_dropped_property_is_present() {
+ Map<String, String> props = loadProps("simple-project");
+ props.put("sonar.qualitygate", "somevalue");
+ AnalysisProperties bootstrapProps = new AnalysisProperties(props, null);
+ new ProjectReactorBuilder(bootstrapProps, mode).execute();
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly("Property 'sonar.qualitygate' is not supported any more. It will be ignored.");
+ }
+
+ private Map<String, String> loadPropsFromFile(String filePath) throws IOException {
+ Properties props = new Properties();
+ try (FileInputStream fileInputStream = new FileInputStream(getResource(this.getClass(), filePath))) {
+ props.load(fileInputStream);
+ }
+ Map<String, String> result = new HashMap<>();
+ for (Map.Entry<Object, Object> entry : props.entrySet()) {
+ result.put(entry.getKey().toString(), entry.getValue().toString());
+ }
+ return result;
+ }
+
+ /**
+ * Search for a test resource in the classpath. For example getResource("org/sonar/MyClass/foo.txt");
+ *
+ * @param path the starting slash is optional
+ * @return the resource. Null if resource not found
+ */
+ public static File getResource(String path) {
+ String resourcePath = path;
+ if (!resourcePath.startsWith("/")) {
+ resourcePath = "/" + resourcePath;
+ }
+ URL url = ProjectReactorBuilderTest.class.getResource(resourcePath);
+ if (url != null) {
+ return FileUtils.toFile(url);
+ }
+ return null;
+ }
+
+ /**
+ * Search for a resource in the classpath. For example calling the method getResource(getClass(), "myTestName/foo.txt") from
+ * the class org.sonar.Foo loads the file $basedir/src/test/resources/org/sonar/Foo/myTestName/foo.txt
+ *
+ * @return the resource. Null if resource not found
+ */
+ public static File getResource(Class baseClass, String path) {
+ String resourcePath = StringUtils.replaceChars(baseClass.getCanonicalName(), '.', '/');
+ if (!path.startsWith("/")) {
+ resourcePath += "/";
+ }
+ resourcePath += path;
+ return getResource(resourcePath);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorValidatorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorValidatorTest.java
new file mode 100644
index 00000000000..fd5882467b5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectReactorValidatorTest.java
@@ -0,0 +1,185 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import static org.mockito.Mockito.when;
+
+import org.sonar.api.utils.MessageException;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.config.Settings;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import static org.mockito.Mockito.mock;
+
+public class ProjectReactorValidatorTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private ProjectReactorValidator validator;
+ private Settings settings;
+ private DefaultAnalysisMode mode;
+
+ @Before
+ public void prepare() {
+ mode = mock(DefaultAnalysisMode.class);
+ settings = new Settings();
+ validator = new ProjectReactorValidator(settings, mode);
+ }
+
+ @Test
+ public void not_fail_with_valid_key() {
+ validator.validate(createProjectReactor("foo"));
+ validator.validate(createProjectReactor("123foo"));
+ validator.validate(createProjectReactor("foo123"));
+ validator.validate(createProjectReactor("1Z3"));
+ validator.validate(createProjectReactor("a123"));
+ validator.validate(createProjectReactor("123a"));
+ validator.validate(createProjectReactor("1:2"));
+ validator.validate(createProjectReactor("3-3"));
+ validator.validate(createProjectReactor("-:"));
+ }
+
+ @Test
+ public void allow_slash_issues_mode() {
+ when(mode.isIssues()).thenReturn(true);
+ validator.validate(createProjectReactor("project/key"));
+
+ when(mode.isIssues()).thenReturn(false);
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("is not a valid project or module key");
+ validator.validate(createProjectReactor("project/key"));
+ }
+
+ @Test
+ public void not_fail_with_alphanumeric_key() {
+ ProjectReactor reactor = createProjectReactor("Foobar2");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void should_not_fail_with_dot_key() {
+ ProjectReactor reactor = createProjectReactor("foo.bar");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void not_fail_with_dash_key() {
+ ProjectReactor reactor = createProjectReactor("foo-bar");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void not_fail_with_colon_key() {
+ ProjectReactor reactor = createProjectReactor("foo:bar");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void not_fail_with_underscore_key() {
+ ProjectReactor reactor = createProjectReactor("foo_bar");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void fail_with_invalid_key() {
+ ProjectReactor reactor = createProjectReactor("foo$bar");
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("\"foo$bar\" is not a valid project or module key");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void fail_with_backslash_in_key() {
+ ProjectReactor reactor = createProjectReactor("foo\\bar");
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("\"foo\\bar\" is not a valid project or module key");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void not_fail_with_valid_branch() {
+ validator.validate(createProjectReactor("foo", "branch"));
+ validator.validate(createProjectReactor("foo", "Branch2"));
+ validator.validate(createProjectReactor("foo", "bra.nch"));
+ validator.validate(createProjectReactor("foo", "bra-nch"));
+ validator.validate(createProjectReactor("foo", "1"));
+ validator.validate(createProjectReactor("foo", "bra_nch"));
+ }
+
+ @Test
+ public void fail_with_invalid_branch() {
+ ProjectReactor reactor = createProjectReactor("foo", "bran#ch");
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("\"bran#ch\" is not a valid branch name");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void fail_with_colon_in_branch() {
+ ProjectReactor reactor = createProjectReactor("foo", "bran:ch");
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("\"bran:ch\" is not a valid branch name");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void fail_with_only_digits() {
+ ProjectReactor reactor = createProjectReactor("12345");
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("\"12345\" is not a valid project or module key");
+ validator.validate(reactor);
+ }
+
+ @Test
+ public void fail_with_deprecated_sonar_phase() {
+ ProjectReactor reactor = createProjectReactor("foo");
+ settings.setProperty("sonar.phase", "phase");
+
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("\"sonar.phase\" is deprecated");
+ validator.validate(reactor);
+ }
+
+ private ProjectReactor createProjectReactor(String projectKey) {
+ ProjectDefinition def = ProjectDefinition.create().setProperty(CoreProperties.PROJECT_KEY_PROPERTY, projectKey);
+ ProjectReactor reactor = new ProjectReactor(def);
+ return reactor;
+ }
+
+ private ProjectReactor createProjectReactor(String projectKey, String branch) {
+ ProjectDefinition def = ProjectDefinition.create()
+ .setProperty(CoreProperties.PROJECT_KEY_PROPERTY, projectKey)
+ .setProperty(CoreProperties.PROJECT_BRANCH_PROPERTY, branch);
+ ProjectReactor reactor = new ProjectReactor(def);
+ settings.setProperty(CoreProperties.PROJECT_BRANCH_PROPERTY, branch);
+ return reactor;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java
new file mode 100644
index 00000000000..512462923a1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.junit.Test;
+import org.sonar.api.BatchExtension;
+import org.sonar.api.ServerExtension;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.task.TaskExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectScanContainerTest {
+
+ @Test
+ public void should_add_only_batch_extensions() {
+ ProjectScanContainer.BatchExtensionFilter filter = new ProjectScanContainer.BatchExtensionFilter();
+
+ assertThat(filter.accept(new MyBatchExtension())).isTrue();
+ assertThat(filter.accept(MyBatchExtension.class)).isTrue();
+
+ assertThat(filter.accept(new MyProjectExtension())).isFalse();
+ assertThat(filter.accept(MyProjectExtension.class)).isFalse();
+ assertThat(filter.accept(new MyServerExtension())).isFalse();
+ assertThat(filter.accept(MyServerExtension.class)).isFalse();
+ assertThat(filter.accept(new MyTaskExtension())).isFalse();
+ assertThat(filter.accept(MyTaskExtension.class)).isFalse();
+ }
+
+ @InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+ static class MyBatchExtension implements BatchExtension {
+
+ }
+
+ static class MyProjectExtension implements BatchExtension {
+
+ }
+
+ static class MyServerExtension implements ServerExtension {
+
+ }
+
+ static class MyTaskExtension implements TaskExtension {
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java
new file mode 100644
index 00000000000..7f9c6eecc4c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import org.sonar.batch.bootstrap.GlobalMode;
+import org.sonar.batch.bootstrap.GlobalProperties;
+import org.sonar.batch.bootstrap.GlobalSettings;
+import org.sonar.batch.repository.FileData;
+import org.sonar.batch.repository.ProjectRepositories;
+import org.sonar.scanner.protocol.input.GlobalRepositories;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProjectSettingsTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private ProjectRepositories projectRef;
+ private ProjectDefinition project;
+ private GlobalSettings bootstrapProps;
+ private Table<String, String, FileData> emptyFileData;
+ private Table<String, String, String> emptySettings;
+
+ private GlobalMode globalMode;
+ private DefaultAnalysisMode mode;
+
+ @Before
+ public void prepare() {
+ emptyFileData = ImmutableTable.of();
+ emptySettings = ImmutableTable.of();
+ project = ProjectDefinition.create().setKey("struts");
+ globalMode = mock(GlobalMode.class);
+ mode = mock(DefaultAnalysisMode.class);
+ bootstrapProps = new GlobalSettings(new GlobalProperties(Collections.<String, String>emptyMap()), new PropertyDefinitions(), new GlobalRepositories(), globalMode);
+ }
+
+ @Test
+ public void should_load_project_props() {
+ project.setProperty("project.prop", "project");
+
+ projectRef = new ProjectRepositories(emptySettings, emptyFileData, null);
+ ProjectSettings batchSettings = new ProjectSettings(new ProjectReactor(project), bootstrapProps, new PropertyDefinitions(), projectRef, mode);
+
+ assertThat(batchSettings.getString("project.prop")).isEqualTo("project");
+ }
+
+ @Test
+ public void should_load_project_root_settings() {
+ Table<String, String, String> settings = HashBasedTable.create();
+ settings.put("struts", "sonar.cpd.cross", "true");
+ settings.put("struts", "sonar.java.coveragePlugin", "jacoco");
+
+ projectRef = new ProjectRepositories(settings, emptyFileData, null);
+ ProjectSettings batchSettings = new ProjectSettings(new ProjectReactor(project), bootstrapProps, new PropertyDefinitions(), projectRef, mode);
+ assertThat(batchSettings.getString("sonar.java.coveragePlugin")).isEqualTo("jacoco");
+ }
+
+ @Test
+ public void should_load_project_root_settings_on_branch() {
+ project.setProperty(CoreProperties.PROJECT_BRANCH_PROPERTY, "mybranch");
+
+ Table<String, String, String> settings = HashBasedTable.create();
+ settings.put("struts:mybranch", "sonar.cpd.cross", "true");
+ settings.put("struts:mybranch", "sonar.java.coveragePlugin", "jacoco");
+
+ projectRef = new ProjectRepositories(settings, emptyFileData, null);
+
+ ProjectSettings batchSettings = new ProjectSettings(new ProjectReactor(project), bootstrapProps, new PropertyDefinitions(), projectRef, mode);
+
+ assertThat(batchSettings.getString("sonar.java.coveragePlugin")).isEqualTo("jacoco");
+ }
+
+ @Test
+ public void should_not_fail_when_accessing_secured_properties() {
+ Table<String, String, String> settings = HashBasedTable.create();
+ settings.put("struts", "sonar.foo.secured", "bar");
+ settings.put("struts", "sonar.foo.license.secured", "bar2");
+
+ projectRef = new ProjectRepositories(settings, emptyFileData, null);
+ ProjectSettings batchSettings = new ProjectSettings(new ProjectReactor(project), bootstrapProps, new PropertyDefinitions(), projectRef, mode);
+
+ assertThat(batchSettings.getString("sonar.foo.license.secured")).isEqualTo("bar2");
+ assertThat(batchSettings.getString("sonar.foo.secured")).isEqualTo("bar");
+ }
+
+ @Test
+ public void should_fail_when_accessing_secured_properties_in_issues_mode() {
+ Table<String, String, String> settings = HashBasedTable.create();
+ settings.put("struts", "sonar.foo.secured", "bar");
+ settings.put("struts", "sonar.foo.license.secured", "bar2");
+
+ when(mode.isIssues()).thenReturn(true);
+
+ projectRef = new ProjectRepositories(settings, emptyFileData, null);
+ ProjectSettings batchSettings = new ProjectSettings(new ProjectReactor(project), bootstrapProps, new PropertyDefinitions(), projectRef, mode);
+
+ assertThat(batchSettings.getString("sonar.foo.license.secured")).isEqualTo("bar2");
+ thrown.expect(MessageException.class);
+ thrown
+ .expectMessage(
+ "Access to the secured property 'sonar.foo.secured' is not possible in issues mode. The SonarQube plugin which requires this property must be deactivated in issues mode.");
+ batchSettings.getString("sonar.foo.secured");
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/WorkDirectoryCleanerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/WorkDirectoryCleanerTest.java
new file mode 100644
index 00000000000..8bd3d5c4e3a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/WorkDirectoryCleanerTest.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.home.cache.DirectoryLock;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class WorkDirectoryCleanerTest {
+ private WorkDirectoryCleaner cleaner;
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws IOException {
+ // create files to clean
+ temp.newFile();
+ File newFolder = temp.newFolder();
+ File fileInFolder = new File(newFolder, "test");
+ fileInFolder.createNewFile();
+
+ File lock = new File(temp.getRoot(), DirectoryLock.LOCK_FILE_NAME);
+ lock.createNewFile();
+
+ // mock project
+ ProjectReactor projectReactor = mock(ProjectReactor.class);
+ ProjectDefinition projectDefinition = mock(ProjectDefinition.class);
+ when(projectReactor.getRoot()).thenReturn(projectDefinition);
+ when(projectDefinition.getWorkDir()).thenReturn(temp.getRoot());
+
+ assertThat(temp.getRoot().list().length).isGreaterThan(1);
+ cleaner = new WorkDirectoryCleaner(projectReactor);
+ }
+
+ @Test
+ public void testNonExisting() {
+ temp.delete();
+ cleaner.execute();
+ }
+
+ @Test
+ public void testClean() {
+ File lock = new File(temp.getRoot(), DirectoryLock.LOCK_FILE_NAME);
+ cleaner.execute();
+
+ assertThat(temp.getRoot()).exists();
+ assertThat(lock).exists();
+ assertThat(temp.getRoot().list()).containsOnly(DirectoryLock.LOCK_FILE_NAME);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicatesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicatesTest.java
new file mode 100644
index 00000000000..c0f9fd3c062
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/AdditionalFilePredicatesTest.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AdditionalFilePredicatesTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void key() {
+ FilePredicate predicate = new AdditionalFilePredicates.KeyPredicate("struts:Action.java");
+
+ DefaultInputFile inputFile = new DefaultInputFile("struts", "Action.java");
+ assertThat(predicate.apply(inputFile)).isTrue();
+
+ inputFile = new DefaultInputFile("struts", "Filter.java");
+ assertThat(predicate.apply(inputFile)).isFalse();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ComponentIndexerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ComponentIndexerTest.java
new file mode 100644
index 00000000000..554f175695f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ComponentIndexerTest.java
@@ -0,0 +1,141 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.fs.InputFile.Status;
+
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentMatcher;
+import org.sonar.api.batch.SonarIndex;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.resources.AbstractLanguage;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.Java;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Resource;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ComponentIndexerTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ private File baseDir;
+ private DefaultFileSystem fs;
+ private SonarIndex sonarIndex;
+ private AbstractLanguage cobolLanguage;
+ private Project project;
+ private ModuleFileSystemInitializer initializer;
+ private DefaultAnalysisMode mode;
+
+ @Before
+ public void prepare() throws IOException {
+ baseDir = temp.newFolder();
+ fs = new DefaultFileSystem(baseDir.toPath());
+ sonarIndex = mock(SonarIndex.class);
+ project = new Project("myProject");
+ initializer = mock(ModuleFileSystemInitializer.class);
+ mode = mock(DefaultAnalysisMode.class);
+ when(initializer.baseDir()).thenReturn(baseDir);
+ when(initializer.workingDir()).thenReturn(temp.newFolder());
+ cobolLanguage = new AbstractLanguage("cobol") {
+ @Override
+ public String[] getFileSuffixes() {
+ return new String[] {"cbl"};
+ }
+ };
+ }
+
+ @Test
+ public void should_index_java_files() throws IOException {
+ Languages languages = new Languages(Java.INSTANCE);
+ ComponentIndexer indexer = createIndexer(languages);
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(project, null, mock(FileIndexer.class), initializer, indexer, mode);
+ fs.add(newInputFile("src/main/java/foo/bar/Foo.java", "", "foo/bar/Foo.java", "java", false, Status.ADDED));
+ fs.add(newInputFile("src/main/java2/foo/bar/Foo.java", "", "foo/bar/Foo.java", "java", false, Status.ADDED));
+ // should index even if filter is applied
+ fs.add(newInputFile("src/test/java/foo/bar/FooTest.java", "", "foo/bar/FooTest.java", "java", true, Status.SAME));
+
+ fs.index();
+
+ verify(sonarIndex).index(org.sonar.api.resources.File.create("src/main/java/foo/bar/Foo.java", Java.INSTANCE, false));
+ verify(sonarIndex).index(org.sonar.api.resources.File.create("src/main/java2/foo/bar/Foo.java", Java.INSTANCE, false));
+ verify(sonarIndex).index(argThat(new ArgumentMatcher<org.sonar.api.resources.File>() {
+ @Override
+ public boolean matches(Object arg0) {
+ org.sonar.api.resources.File javaFile = (org.sonar.api.resources.File) arg0;
+ return javaFile.getKey().equals("src/test/java/foo/bar/FooTest.java")
+ && javaFile.getPath().equals("src/test/java/foo/bar/FooTest.java")
+ && javaFile.getQualifier().equals(Qualifiers.UNIT_TEST_FILE);
+ }
+ }));
+ }
+
+ private ComponentIndexer createIndexer(Languages languages) {
+ BatchComponentCache resourceCache = mock(BatchComponentCache.class);
+ when(resourceCache.get(any(Resource.class)))
+ .thenReturn(new BatchComponent(2, org.sonar.api.resources.File.create("foo.php"), new BatchComponent(1, Directory.create("src"), null)));
+ return new ComponentIndexer(project, languages, sonarIndex, resourceCache);
+ }
+
+ @Test
+ public void should_index_cobol_files() throws IOException {
+ Languages languages = new Languages(cobolLanguage);
+ ComponentIndexer indexer = createIndexer(languages);
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(project, null, mock(FileIndexer.class), initializer, indexer, mode);
+ fs.add(newInputFile("src/foo/bar/Foo.cbl", "", "foo/bar/Foo.cbl", "cobol", false, Status.ADDED));
+ fs.add(newInputFile("src2/foo/bar/Foo.cbl", "", "foo/bar/Foo.cbl", "cobol", false, Status.ADDED));
+ fs.add(newInputFile("src/test/foo/bar/FooTest.cbl", "", "foo/bar/FooTest.cbl", "cobol", true, Status.ADDED));
+
+ fs.index();
+
+ verify(sonarIndex).index(org.sonar.api.resources.File.create("/src/foo/bar/Foo.cbl", cobolLanguage, false));
+ verify(sonarIndex).index(org.sonar.api.resources.File.create("/src2/foo/bar/Foo.cbl", cobolLanguage, false));
+ verify(sonarIndex).index(org.sonar.api.resources.File.create("/src/test/foo/bar/FooTest.cbl", cobolLanguage, true));
+ }
+
+ private DefaultInputFile newInputFile(String path, String content, String sourceRelativePath, String languageKey, boolean unitTest, InputFile.Status status) throws IOException {
+ File file = new File(baseDir, path);
+ FileUtils.write(file, content);
+ return new DefaultInputFile("foo", path)
+ .setLanguage(languageKey)
+ .setType(unitTest ? InputFile.Type.TEST : InputFile.Type.MAIN)
+ .setStatus(status);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java
new file mode 100644
index 00000000000..46b70a17522
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java
@@ -0,0 +1,200 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.api.batch.fs.InputFile.Status;
+
+import org.junit.Before;
+import org.sonar.batch.analysis.DefaultAnalysisMode;
+import com.google.common.collect.Lists;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+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 DefaultModuleFileSystemTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private Settings settings;
+ private FileIndexer fileIndexer;
+ private ModuleFileSystemInitializer initializer;
+ private ComponentIndexer componentIndexer;
+ private ModuleInputFileCache moduleInputFileCache;
+ private DefaultAnalysisMode mode;
+
+ @Before
+ public void setUp() {
+ settings = new Settings();
+ fileIndexer = mock(FileIndexer.class);
+ initializer = mock(ModuleFileSystemInitializer.class, Mockito.RETURNS_DEEP_STUBS);
+ componentIndexer = mock(ComponentIndexer.class);
+ moduleInputFileCache = mock(ModuleInputFileCache.class);
+ mode = mock(DefaultAnalysisMode.class);
+ }
+
+ @Test
+ public void test_equals_and_hashCode() throws Exception {
+ DefaultModuleFileSystem foo1 = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+ DefaultModuleFileSystem foo2 = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+ DefaultModuleFileSystem bar = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("bar"), settings, fileIndexer, initializer, componentIndexer, mode);
+ DefaultModuleFileSystem branch = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("bar", "branch", "My project"), settings, fileIndexer, initializer, componentIndexer, mode);
+
+ assertThat(foo1.moduleKey()).isEqualTo("foo");
+ assertThat(branch.moduleKey()).isEqualTo("bar:branch");
+ assertThat(foo1.equals(foo1)).isTrue();
+ assertThat(foo1.equals(foo2)).isTrue();
+ assertThat(foo1.equals(bar)).isFalse();
+ assertThat(foo1.equals("foo")).isFalse();
+ assertThat(foo1.hashCode()).isEqualTo(foo1.hashCode());
+ assertThat(foo1.hashCode()).isEqualTo(foo2.hashCode());
+ }
+
+ @Test
+ public void default_source_encoding() {
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+
+ assertThat(fs.sourceCharset()).isEqualTo(Charset.defaultCharset());
+ assertThat(fs.isDefaultJvmEncoding()).isTrue();
+ }
+
+ @Test
+ public void source_encoding_is_set() {
+ settings.setProperty(CoreProperties.ENCODING_PROPERTY, "Cp1124");
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+
+ assertThat(fs.encoding()).isEqualTo(Charset.forName("Cp1124"));
+ assertThat(fs.sourceCharset()).isEqualTo(Charset.forName("Cp1124"));
+
+ // This test fails when default Java encoding is "IBM AIX Ukraine". Sorry for that.
+ assertThat(fs.isDefaultJvmEncoding()).isFalse();
+ }
+
+ @Test
+ public void default_predicate_scan_only_changed() throws IOException {
+ when(mode.scanAllFiles()).thenReturn(false);
+
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+
+ File baseDir = temp.newFile();
+ InputFile mainInput = new DefaultInputFile("foo", "Main.java").setModuleBaseDir(baseDir.toPath()).setType(InputFile.Type.MAIN);
+ InputFile testInput = new DefaultInputFile("foo", "Test.java").setModuleBaseDir(baseDir.toPath()).setType(InputFile.Type.TEST);
+ InputFile mainSameInput = new DefaultInputFile("foo", "MainSame.java").setModuleBaseDir(baseDir.toPath())
+ .setType(InputFile.Type.TEST).setStatus(Status.SAME);
+ when(moduleInputFileCache.inputFiles()).thenReturn(Lists.newArrayList(mainInput, testInput, mainSameInput));
+
+ fs.index();
+ Iterable<InputFile> inputFiles = fs.inputFiles(fs.predicates().all());
+ assertThat(inputFiles).containsOnly(mainInput, testInput);
+
+ Iterable<InputFile> allInputFiles = fs.inputFiles();
+ assertThat(allInputFiles).containsOnly(mainInput, mainSameInput, testInput);
+ }
+
+ @Test
+ public void test_dirs() throws IOException {
+ File basedir = temp.newFolder("base");
+ File buildDir = temp.newFolder("build");
+ File workingDir = temp.newFolder("work");
+ File additionalFile = temp.newFile("Main.java");
+ File additionalTest = temp.newFile("Test.java");
+ when(initializer.baseDir()).thenReturn(basedir);
+ when(initializer.buildDir()).thenReturn(buildDir);
+ when(initializer.workingDir()).thenReturn(workingDir);
+ when(initializer.binaryDirs()).thenReturn(Arrays.asList(new File(basedir, "target/classes")));
+ File javaSrc = new File(basedir, "src/main/java");
+ javaSrc.mkdirs();
+ File groovySrc = new File(basedir, "src/main/groovy");
+ groovySrc.mkdirs();
+ when(initializer.sources()).thenReturn(Arrays.asList(javaSrc, groovySrc, additionalFile));
+ File javaTest = new File(basedir, "src/test/java");
+ javaTest.mkdirs();
+ when(initializer.tests()).thenReturn(Arrays.asList(javaTest, additionalTest));
+
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+
+ assertThat(fs.baseDir().getCanonicalPath()).isEqualTo(basedir.getCanonicalPath());
+ assertThat(fs.workDir().getCanonicalPath()).isEqualTo(workingDir.getCanonicalPath());
+ assertThat(fs.buildDir().getCanonicalPath()).isEqualTo(buildDir.getCanonicalPath());
+ assertThat(fs.sourceDirs()).hasSize(2);
+ assertThat(fs.testDirs()).hasSize(1);
+ assertThat(fs.binaryDirs()).hasSize(1);
+ }
+
+ @Test
+ public void should_search_input_files() throws Exception {
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+
+ File baseDir = temp.newFile();
+ InputFile mainInput = new DefaultInputFile("foo", "Main.java").setModuleBaseDir(baseDir.toPath()).setType(InputFile.Type.MAIN);
+ InputFile testInput = new DefaultInputFile("foo", "Test.java").setModuleBaseDir(baseDir.toPath()).setType(InputFile.Type.TEST);
+ when(moduleInputFileCache.inputFiles()).thenReturn(Lists.newArrayList(mainInput, testInput));
+
+ fs.index();
+ Iterable<InputFile> inputFiles = fs.inputFiles(fs.predicates().hasType(InputFile.Type.MAIN));
+ assertThat(inputFiles).containsOnly(mainInput);
+
+ Iterable<File> files = fs.files(fs.predicates().hasType(InputFile.Type.MAIN));
+ assertThat(files).containsOnly(new File(baseDir, "Main.java"));
+ }
+
+ @Test
+ public void should_index() {
+ DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
+ new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
+
+ verifyZeroInteractions(fileIndexer);
+
+ fs.index();
+ verify(fileIndexer).index(fs);
+ verify(componentIndexer).execute(fs);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DeprecatedFileFiltersTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DeprecatedFileFiltersTest.java
new file mode 100644
index 00000000000..e1905a12a9e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/DeprecatedFileFiltersTest.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.apache.commons.io.FilenameUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.scan.filesystem.FileSystemFilter;
+import org.sonar.api.scan.filesystem.FileType;
+
+import java.io.File;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DeprecatedFileFiltersTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ FileSystemFilter filter = mock(FileSystemFilter.class);
+
+ @Test
+ public void no_filters() {
+ DeprecatedFileFilters filters = new DeprecatedFileFilters();
+
+ InputFile inputFile = new DefaultInputFile("foo", "src/main/java/Foo.java");
+ assertThat(filters.accept(inputFile)).isTrue();
+ }
+
+ @Test
+ public void at_least_one_filter() throws Exception {
+ DeprecatedFileFilters filters = new DeprecatedFileFilters(new FileSystemFilter[] {filter});
+
+ File basedir = temp.newFolder();
+ File file = new File(basedir, "src/main/java/Foo.java");
+ InputFile inputFile = new DefaultInputFile("foo", "src/main/java/Foo.java")
+ .setModuleBaseDir(basedir.toPath())
+ .setType(InputFile.Type.MAIN);
+ when(filter.accept(eq(file), any(DeprecatedFileFilters.DeprecatedContext.class))).thenReturn(false);
+
+ assertThat(filters.accept(inputFile)).isFalse();
+
+ ArgumentCaptor<DeprecatedFileFilters.DeprecatedContext> argument = ArgumentCaptor.forClass(DeprecatedFileFilters.DeprecatedContext.class);
+ verify(filter).accept(eq(file), argument.capture());
+
+ DeprecatedFileFilters.DeprecatedContext context = argument.getValue();
+ assertThat(context.canonicalPath()).isEqualTo(FilenameUtils.separatorsToUnix(file.getAbsolutePath()));
+ assertThat(context.relativePath()).isEqualTo("src/main/java/Foo.java");
+ assertThat(context.type()).isEqualTo(FileType.MAIN);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ExclusionFiltersTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ExclusionFiltersTest.java
new file mode 100644
index 00000000000..c7b4e6eaa5e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ExclusionFiltersTest.java
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.api.scan.filesystem.FileExclusions;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExclusionFiltersTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void no_inclusions_nor_exclusions() throws IOException {
+ ExclusionFilters filter = new ExclusionFilters(new FileExclusions(new Settings()));
+ filter.prepare();
+
+ java.io.File file = temp.newFile();
+ DefaultInputFile inputFile = new DefaultInputFile("foo", "src/main/java/com/mycompany/FooDao.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isTrue();
+ assertThat(filter.accept(inputFile, InputFile.Type.TEST)).isTrue();
+ }
+
+ @Test
+ public void match_inclusion() throws IOException {
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_INCLUSIONS_PROPERTY, "**/*Dao.java");
+ ExclusionFilters filter = new ExclusionFilters(new FileExclusions(settings));
+ filter.prepare();
+
+ java.io.File file = temp.newFile();
+ DefaultInputFile inputFile = new DefaultInputFile("foo", "src/main/java/com/mycompany/FooDao.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isTrue();
+
+ inputFile = new DefaultInputFile("foo", "src/main/java/com/mycompany/Foo.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isFalse();
+ }
+
+ @Test
+ public void match_at_least_one_inclusion() throws IOException {
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_INCLUSIONS_PROPERTY, "**/*Dao.java,**/*Dto.java");
+ ExclusionFilters filter = new ExclusionFilters(new FileExclusions(settings));
+
+ filter.prepare();
+
+ java.io.File file = temp.newFile();
+
+ DefaultInputFile inputFile = new DefaultInputFile("foo", "src/main/java/com/mycompany/Foo.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isFalse();
+
+ inputFile = new DefaultInputFile("foo", "src/main/java/com/mycompany/FooDto.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isTrue();
+ }
+
+ @Test
+ public void match_exclusions() throws IOException {
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_INCLUSIONS_PROPERTY, "src/main/java/**/*");
+ settings.setProperty(CoreProperties.PROJECT_TEST_INCLUSIONS_PROPERTY, "src/test/java/**/*");
+ settings.setProperty(CoreProperties.PROJECT_EXCLUSIONS_PROPERTY, "**/*Dao.java");
+ ExclusionFilters filter = new ExclusionFilters(new FileExclusions(settings));
+
+ filter.prepare();
+
+ java.io.File file = temp.newFile();
+ DefaultInputFile inputFile = new DefaultInputFile("foo", "src/main/java/com/mycompany/FooDao.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isFalse();
+
+ inputFile = new DefaultInputFile("foo", "src/main/java/com/mycompany/Foo.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isTrue();
+
+ // source exclusions do not apply to tests
+ inputFile = new DefaultInputFile("foo", "src/test/java/com/mycompany/FooDao.java").setModuleBaseDir(temp.newFolder().toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.TEST)).isTrue();
+ }
+
+ @Test
+ public void match_exclusion_by_absolute_path() throws IOException {
+ File baseDir = temp.newFile();
+ File excludedFile = new File(baseDir, "src/main/java/org/bar/Bar.java");
+
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_INCLUSIONS_PROPERTY, "src/main/java/**/*");
+ settings.setProperty(CoreProperties.PROJECT_EXCLUSIONS_PROPERTY, "file:" + excludedFile.getCanonicalPath());
+ ExclusionFilters filter = new ExclusionFilters(new FileExclusions(settings));
+
+ filter.prepare();
+
+ DefaultInputFile inputFile = new DefaultInputFile("foo", "src/main/java/org/bar/Foo.java").setModuleBaseDir(baseDir.toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isTrue();
+
+ inputFile = new DefaultInputFile("foo", "src/main/java/org/bar/Bar.java").setModuleBaseDir(baseDir.toPath());
+ assertThat(filter.accept(inputFile, InputFile.Type.MAIN)).isFalse();
+ }
+
+ @Test
+ public void trim_pattern() {
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_EXCLUSIONS_PROPERTY, " **/*Dao.java ");
+ ExclusionFilters filter = new ExclusionFilters(new FileExclusions(settings));
+
+ assertThat(filter.prepareMainExclusions()[0].toString()).isEqualTo("**/*Dao.java");
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactoryTest.java
new file mode 100644
index 00000000000..b9c627e1ea8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderFactoryTest.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.config.Settings;
+import org.sonar.api.scan.filesystem.PathResolver;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class InputFileBuilderFactoryTest {
+ @Test
+ public void create_builder() {
+ PathResolver pathResolver = new PathResolver();
+ LanguageDetectionFactory langDetectionFactory = mock(LanguageDetectionFactory.class, Mockito.RETURNS_MOCKS);
+ StatusDetectionFactory statusDetectionFactory = mock(StatusDetectionFactory.class, Mockito.RETURNS_MOCKS);
+ DefaultModuleFileSystem fs = mock(DefaultModuleFileSystem.class);
+
+ InputFileBuilderFactory factory = new InputFileBuilderFactory(ProjectDefinition.create().setKey("struts"), pathResolver, langDetectionFactory,
+ statusDetectionFactory, new Settings(), new FileMetadata());
+ InputFileBuilder builder = factory.create(fs);
+
+ assertThat(builder.langDetection()).isNotNull();
+ assertThat(builder.statusDetection()).isNotNull();
+ assertThat(builder.pathResolver()).isSameAs(pathResolver);
+ assertThat(builder.fs()).isSameAs(fs);
+ assertThat(builder.moduleKey()).isEqualTo("struts");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderTest.java
new file mode 100644
index 00000000000..eaec22ca37a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputFileBuilderTest.java
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.config.Settings;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.PathUtils;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class InputFileBuilderTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ LanguageDetection langDetection = mock(LanguageDetection.class);
+ StatusDetection statusDetection = mock(StatusDetection.class);
+ DefaultModuleFileSystem fs = mock(DefaultModuleFileSystem.class);
+
+ @Test
+ public void complete_input_file() throws Exception {
+ // file system
+ File basedir = temp.newFolder();
+ File srcFile = new File(basedir, "src/main/java/foo/Bar.java");
+ FileUtils.touch(srcFile);
+ FileUtils.write(srcFile, "single line");
+ when(fs.baseDir()).thenReturn(basedir);
+ when(fs.encoding()).thenReturn(StandardCharsets.UTF_8);
+
+ // lang
+ when(langDetection.language(any(InputFile.class))).thenReturn("java");
+
+ // status
+ when(statusDetection.status("foo", "src/main/java/foo/Bar.java", "6c1d64c0b3555892fe7273e954f6fb5a"))
+ .thenReturn(InputFile.Status.ADDED);
+
+ InputFileBuilder builder = new InputFileBuilder("struts", new PathResolver(),
+ langDetection, statusDetection, fs, new Settings(), new FileMetadata());
+ DefaultInputFile inputFile = builder.create(srcFile);
+ builder.completeAndComputeMetadata(inputFile, InputFile.Type.MAIN);
+
+ assertThat(inputFile.type()).isEqualTo(InputFile.Type.MAIN);
+ assertThat(inputFile.file()).isEqualTo(srcFile.getAbsoluteFile());
+ assertThat(inputFile.absolutePath()).isEqualTo(PathUtils.sanitize(srcFile.getAbsolutePath()));
+ assertThat(inputFile.language()).isEqualTo("java");
+ assertThat(inputFile.key()).isEqualTo("struts:src/main/java/foo/Bar.java");
+ assertThat(inputFile.relativePath()).isEqualTo("src/main/java/foo/Bar.java");
+ assertThat(inputFile.lines()).isEqualTo(1);
+ }
+
+ @Test
+ public void return_null_if_file_outside_basedir() throws Exception {
+ // file system
+ File basedir = temp.newFolder();
+ File otherDir = temp.newFolder();
+ File srcFile = new File(otherDir, "src/main/java/foo/Bar.java");
+ FileUtils.touch(srcFile);
+ when(fs.baseDir()).thenReturn(basedir);
+
+ InputFileBuilder builder = new InputFileBuilder("struts", new PathResolver(),
+ langDetection, statusDetection, fs, new Settings(), new FileMetadata());
+ DefaultInputFile inputFile = builder.create(srcFile);
+
+ assertThat(inputFile).isNull();
+ }
+
+ @Test
+ public void return_null_if_language_not_detected() throws Exception {
+ // file system
+ File basedir = temp.newFolder();
+ File srcFile = new File(basedir, "src/main/java/foo/Bar.java");
+ FileUtils.touch(srcFile);
+ FileUtils.write(srcFile, "single line");
+ when(fs.baseDir()).thenReturn(basedir);
+ when(fs.encoding()).thenReturn(StandardCharsets.UTF_8);
+
+ // lang
+ when(langDetection.language(any(InputFile.class))).thenReturn(null);
+
+ InputFileBuilder builder = new InputFileBuilder("struts", new PathResolver(),
+ langDetection, statusDetection, fs, new Settings(), new FileMetadata());
+ DefaultInputFile inputFile = builder.create(srcFile);
+ inputFile = builder.completeAndComputeMetadata(inputFile, InputFile.Type.MAIN);
+
+ assertThat(inputFile).isNull();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputPathCacheTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputPathCacheTest.java
new file mode 100644
index 00000000000..23269eef7d3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/InputPathCacheTest.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile.Status;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.batch.fs.InputPath;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InputPathCacheTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void start() {
+ }
+
+ @After
+ public void stop() {
+ }
+
+ @Test
+ public void should_add_input_file() throws Exception {
+ InputPathCache cache = new InputPathCache();
+ DefaultInputFile fooFile = new DefaultInputFile("foo", "src/main/java/Foo.java").setModuleBaseDir(temp.newFolder().toPath());
+ cache.put("struts", fooFile);
+ cache.put("struts-core", new DefaultInputFile("foo", "src/main/java/Bar.java")
+ .setLanguage("bla")
+ .setType(Type.MAIN)
+ .setStatus(Status.ADDED)
+ .setLines(2)
+ .setCharset(StandardCharsets.UTF_8)
+ .setModuleBaseDir(temp.newFolder().toPath()));
+
+ DefaultInputFile loadedFile = (DefaultInputFile) cache.getFile("struts-core", "src/main/java/Bar.java");
+ assertThat(loadedFile.relativePath()).isEqualTo("src/main/java/Bar.java");
+ assertThat(loadedFile.charset()).isEqualTo(StandardCharsets.UTF_8);
+
+ assertThat(cache.filesByModule("struts")).hasSize(1);
+ assertThat(cache.filesByModule("struts-core")).hasSize(1);
+ assertThat(cache.allFiles()).hasSize(2);
+ for (InputPath inputPath : cache.allFiles()) {
+ assertThat(inputPath.relativePath()).startsWith("src/main/java/");
+ }
+
+ cache.remove("struts", fooFile);
+ assertThat(cache.allFiles()).hasSize(1);
+
+ cache.removeModule("struts");
+ assertThat(cache.filesByModule("struts")).hasSize(0);
+ assertThat(cache.filesByModule("struts-core")).hasSize(1);
+ assertThat(cache.allFiles()).hasSize(1);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactoryTest.java
new file mode 100644
index 00000000000..b30bb73036c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionFactoryTest.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Java;
+import org.sonar.api.resources.Languages;
+import org.sonar.batch.repository.language.DefaultLanguagesRepository;
+import org.sonar.batch.repository.language.LanguagesRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LanguageDetectionFactoryTest {
+ @Test
+ public void testCreate() throws Exception {
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(Java.INSTANCE));
+ LanguageDetectionFactory factory = new LanguageDetectionFactory(new Settings(), languages);
+ LanguageDetection languageDetection = factory.create();
+ assertThat(languageDetection).isNotNull();
+ assertThat(languageDetection.patternsByLanguage()).hasSize(1);
+ assertThat(languageDetection.patternsByLanguage().containsKey("java")).isTrue();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionTest.java
new file mode 100644
index 00000000000..01a6a8aef3c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/LanguageDetectionTest.java
@@ -0,0 +1,213 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.utils.MessageException;
+import org.sonar.batch.repository.language.DefaultLanguagesRepository;
+import org.sonar.batch.repository.language.LanguagesRepository;
+
+import java.io.File;
+import java.io.IOException;
+
+import static junit.framework.Assert.fail;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+
+public class LanguageDetectionTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void test_sanitizeExtension() throws Exception {
+ assertThat(LanguageDetection.sanitizeExtension(".cbl")).isEqualTo("cbl");
+ assertThat(LanguageDetection.sanitizeExtension(".CBL")).isEqualTo("cbl");
+ assertThat(LanguageDetection.sanitizeExtension("CBL")).isEqualTo("cbl");
+ assertThat(LanguageDetection.sanitizeExtension("cbl")).isEqualTo("cbl");
+ }
+
+ @Test
+ public void search_by_file_extension() throws Exception {
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("java", "java", "jav"), new MockLanguage("cobol", "cbl", "cob")));
+ LanguageDetection detection = new LanguageDetection(new Settings(), languages);
+
+ assertThat(detection.language(newInputFile("Foo.java"))).isEqualTo("java");
+ assertThat(detection.language(newInputFile("src/Foo.java"))).isEqualTo("java");
+ assertThat(detection.language(newInputFile("Foo.JAVA"))).isEqualTo("java");
+ assertThat(detection.language(newInputFile("Foo.jav"))).isEqualTo("java");
+ assertThat(detection.language(newInputFile("Foo.Jav"))).isEqualTo("java");
+
+ assertThat(detection.language(newInputFile("abc.cbl"))).isEqualTo("cobol");
+ assertThat(detection.language(newInputFile("abc.CBL"))).isEqualTo("cobol");
+
+ assertThat(detection.language(newInputFile("abc.php"))).isNull();
+ assertThat(detection.language(newInputFile("abc"))).isNull();
+ }
+
+ @Test
+ public void should_not_fail_if_no_language() throws Exception {
+ LanguageDetection detection = spy(new LanguageDetection(new Settings(), new DefaultLanguagesRepository(new Languages())));
+ assertThat(detection.language(newInputFile("Foo.java"))).isNull();
+ }
+
+ @Test
+ public void plugin_can_declare_a_file_extension_twice_for_case_sensitivity() throws Exception {
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("abap", "abap", "ABAP")));
+
+ LanguageDetection detection = new LanguageDetection(new Settings(), languages);
+ assertThat(detection.language(newInputFile("abc.abap"))).isEqualTo("abap");
+ }
+
+ @Test
+ public void language_with_no_extension() throws Exception {
+ // abap does not declare any file extensions.
+ // When analyzing an ABAP project, then all source files must be parsed.
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("java", "java"), new MockLanguage("abap")));
+
+ // No side-effect on non-ABAP projects
+ LanguageDetection detection = new LanguageDetection(new Settings(), languages);
+ assertThat(detection.language(newInputFile("abc"))).isNull();
+ assertThat(detection.language(newInputFile("abc.abap"))).isNull();
+ assertThat(detection.language(newInputFile("abc.java"))).isEqualTo("java");
+
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, "abap");
+ detection = new LanguageDetection(settings, languages);
+ assertThat(detection.language(newInputFile("abc"))).isEqualTo("abap");
+ assertThat(detection.language(newInputFile("abc.txt"))).isEqualTo("abap");
+ assertThat(detection.language(newInputFile("abc.java"))).isEqualTo("abap");
+ }
+
+ @Test
+ public void force_language_using_deprecated_property() throws Exception {
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("java", "java"), new MockLanguage("php", "php")));
+
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, "java");
+ LanguageDetection detection = new LanguageDetection(settings, languages);
+ assertThat(detection.language(newInputFile("abc"))).isNull();
+ assertThat(detection.language(newInputFile("abc.php"))).isNull();
+ assertThat(detection.language(newInputFile("abc.java"))).isEqualTo("java");
+ assertThat(detection.language(newInputFile("src/abc.java"))).isEqualTo("java");
+ }
+
+ @Test
+ public void fail_if_invalid_language() {
+ thrown.expect(MessageException.class);
+ thrown.expectMessage("No language is installed with key 'unknown'. Please update property 'sonar.language'");
+
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("java", "java"), new MockLanguage("php", "php")));
+ Settings settings = new Settings();
+ settings.setProperty(CoreProperties.PROJECT_LANGUAGE_PROPERTY, "unknown");
+ new LanguageDetection(settings, languages);
+ }
+
+ @Test
+ public void fail_if_conflicting_language_suffix() throws Exception {
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml")));
+ LanguageDetection detection = new LanguageDetection(new Settings(), languages);
+ try {
+ detection.language(newInputFile("abc.xhtml"));
+ fail();
+ } catch (MessageException e) {
+ assertThat(e.getMessage())
+ .contains("Language of file 'abc.xhtml' can not be decided as the file matches patterns of both ")
+ .contains("sonar.lang.patterns.web : **/*.xhtml")
+ .contains("sonar.lang.patterns.xml : **/*.xhtml");
+ }
+ }
+
+ @Test
+ public void solve_conflict_using_filepattern() throws Exception {
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("xml", "xhtml"), new MockLanguage("web", "xhtml")));
+
+ Settings settings = new Settings();
+ settings.setProperty("sonar.lang.patterns.xml", "xml/**");
+ settings.setProperty("sonar.lang.patterns.web", "web/**");
+ LanguageDetection detection = new LanguageDetection(settings, languages);
+ assertThat(detection.language(newInputFile("xml/abc.xhtml"))).isEqualTo("xml");
+ assertThat(detection.language(newInputFile("web/abc.xhtml"))).isEqualTo("web");
+ }
+
+ @Test
+ public void fail_if_conflicting_filepattern() throws Exception {
+ LanguagesRepository languages = new DefaultLanguagesRepository(new Languages(new MockLanguage("abap", "abap"), new MockLanguage("cobol", "cobol")));
+ Settings settings = new Settings();
+ settings.setProperty("sonar.lang.patterns.abap", "*.abap,*.txt");
+ settings.setProperty("sonar.lang.patterns.cobol", "*.cobol,*.txt");
+
+ LanguageDetection detection = new LanguageDetection(settings, languages);
+
+ assertThat(detection.language(newInputFile("abc.abap"))).isEqualTo("abap");
+ assertThat(detection.language(newInputFile("abc.cobol"))).isEqualTo("cobol");
+ try {
+ detection.language(newInputFile("abc.txt"));
+ fail();
+ } catch (MessageException e) {
+ assertThat(e.getMessage())
+ .contains("Language of file 'abc.txt' can not be decided as the file matches patterns of both ")
+ .contains("sonar.lang.patterns.abap : *.abap,*.txt")
+ .contains("sonar.lang.patterns.cobol : *.cobol,*.txt");
+ }
+ }
+
+ private InputFile newInputFile(String path) throws IOException {
+ File basedir = temp.newFolder();
+ return new DefaultInputFile("foo", path).setModuleBaseDir(basedir.toPath());
+ }
+
+ static class MockLanguage implements Language {
+ private final String key;
+ private final String[] extensions;
+
+ MockLanguage(String key, String... extensions) {
+ this.key = key;
+ this.extensions = extensions;
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public String getName() {
+ return key;
+ }
+
+ @Override
+ public String[] getFileSuffixes() {
+ return extensions;
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializerTest.java
new file mode 100644
index 00000000000..08dcf9197e5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/ModuleFileSystemInitializerTest.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.TempFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class ModuleFileSystemInitializerTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ PathResolver pathResolver = new PathResolver();
+
+ @Test
+ public void test_default_directories() throws Exception {
+ File baseDir = temp.newFolder("base");
+ File workDir = temp.newFolder("work");
+ ProjectDefinition module = ProjectDefinition.create().setBaseDir(baseDir).setWorkDir(workDir);
+
+ ModuleFileSystemInitializer initializer = new ModuleFileSystemInitializer(module, mock(TempFolder.class), pathResolver);
+
+ assertThat(initializer.baseDir().getCanonicalPath()).isEqualTo(baseDir.getCanonicalPath());
+ assertThat(initializer.workingDir().getCanonicalPath()).isEqualTo(workDir.getCanonicalPath());
+ assertThat(initializer.sources()).isEmpty();
+ assertThat(initializer.tests()).isEmpty();
+ }
+
+ @Test
+ public void should_init_directories() throws IOException {
+ File baseDir = temp.newFolder("base");
+ File buildDir = temp.newFolder("build");
+ File sourceDir = new File(baseDir, "src/main/java");
+ FileUtils.forceMkdir(sourceDir);
+ File testDir = new File(baseDir, "src/test/java");
+ FileUtils.forceMkdir(testDir);
+ File binaryDir = new File(baseDir, "target/classes");
+ FileUtils.forceMkdir(binaryDir);
+
+ ProjectDefinition project = ProjectDefinition.create()
+ .setBaseDir(baseDir)
+ .setBuildDir(buildDir)
+ .addSourceDirs("src/main/java", "src/main/unknown")
+ .addTestDirs("src/test/java", "src/test/unknown")
+ .addBinaryDir("target/classes");
+
+ ModuleFileSystemInitializer initializer = new ModuleFileSystemInitializer(project, mock(TempFolder.class), pathResolver);
+
+ assertThat(initializer.baseDir().getCanonicalPath()).isEqualTo(baseDir.getCanonicalPath());
+ assertThat(initializer.buildDir().getCanonicalPath()).isEqualTo(buildDir.getCanonicalPath());
+ assertThat(initializer.sources()).hasSize(1);
+ assertThat(path(initializer.sources().get(0))).endsWith("src/main/java");
+ assertThat(initializer.tests()).hasSize(1);
+ assertThat(path(initializer.tests().get(0))).endsWith("src/test/java");
+ assertThat(initializer.binaryDirs()).hasSize(1);
+ assertThat(path(initializer.binaryDirs().get(0))).endsWith("target/classes");
+ }
+
+ private String path(File f) throws IOException {
+ return FilenameUtils.separatorsToUnix(f.getCanonicalPath());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java
new file mode 100644
index 00000000000..b741f1eb344
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.batch.repository.ProjectRepositories;
+
+import org.junit.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class StatusDetectionFactoryTest {
+ @Test
+ public void testCreate() throws Exception {
+ StatusDetectionFactory factory = new StatusDetectionFactory(mock(ProjectRepositories.class));
+ StatusDetection detection = factory.create();
+ assertThat(detection).isNotNull();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java
new file mode 100644
index 00000000000..0601fa26b3b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.filesystem;
+
+import org.sonar.batch.repository.FileData;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import org.junit.Test;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.batch.repository.ProjectRepositories;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class StatusDetectionTest {
+ @Test
+ public void detect_status() {
+ Table<String, String, String> t = ImmutableTable.of();
+ ProjectRepositories ref = new ProjectRepositories(t, createTable(), null);
+ StatusDetection statusDetection = new StatusDetection(ref);
+
+ assertThat(statusDetection.status("foo", "src/Foo.java", "ABCDE")).isEqualTo(InputFile.Status.SAME);
+ assertThat(statusDetection.status("foo", "src/Foo.java", "XXXXX")).isEqualTo(InputFile.Status.CHANGED);
+ assertThat(statusDetection.status("foo", "src/Other.java", "QWERT")).isEqualTo(InputFile.Status.ADDED);
+ }
+
+ private static Table<String, String, FileData> createTable() {
+ Table<String, String, FileData> t = HashBasedTable.create();
+
+ t.put("foo", "src/Foo.java", new FileData("ABCDE", "12345789"));
+ t.put("foo", "src/Bar.java", new FileData("FGHIJ", "123456789"));
+
+ return t;
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/measure/MeasureCacheTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/measure/MeasureCacheTest.java
new file mode 100644
index 00000000000..61f21529547
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/measure/MeasureCacheTest.java
@@ -0,0 +1,231 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.measure;
+
+import java.util.Date;
+import java.util.Iterator;
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric.Level;
+import org.sonar.api.resources.Directory;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.batch.index.AbstractCachesTest;
+import org.sonar.batch.index.Cache.Entry;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MeasureCacheTest extends AbstractCachesTest {
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private MetricFinder metricFinder;
+
+ private MeasureCache measureCache;
+
+ @Before
+ public void start() {
+ super.start();
+ metricFinder = mock(MetricFinder.class);
+ when(metricFinder.findByKey(CoreMetrics.NCLOC_KEY)).thenReturn(CoreMetrics.NCLOC);
+ measureCache = new MeasureCache(caches, metricFinder);
+ }
+
+ @Test
+ public void should_add_measure() {
+ Project p = new Project("struts");
+
+ assertThat(measureCache.entries()).hasSize(0);
+ assertThat(measureCache.byResource(p)).hasSize(0);
+
+ Measure m = new Measure(CoreMetrics.NCLOC, 1.0);
+ measureCache.put(p, m);
+
+ assertThat(measureCache.contains(p, m)).isTrue();
+ assertThat(measureCache.entries()).hasSize(1);
+ Iterator<Entry<Measure>> iterator = measureCache.entries().iterator();
+ iterator.hasNext();
+ Entry<Measure> next = iterator.next();
+ assertThat(next.value()).isEqualTo(m);
+ assertThat(next.key()[0]).isEqualTo("struts");
+
+ assertThat(measureCache.byResource(p)).hasSize(1);
+ assertThat(measureCache.byResource(p).iterator().next()).isEqualTo(m);
+ }
+
+ @Test
+ public void should_add_measure_with_big_data() {
+ Project p = new Project("struts");
+
+ assertThat(measureCache.entries()).hasSize(0);
+
+ assertThat(measureCache.byResource(p)).hasSize(0);
+
+ Measure m = new Measure(CoreMetrics.NCLOC, 1.0).setDate(new Date());
+ m.setAlertText("foooooooooooooooooooooooooooooooooooo");
+ StringBuilder data = new StringBuilder();
+ for (int i = 0; i < 1_048_575; i++) {
+ data.append("a");
+ }
+
+ m.setData(data.toString());
+
+ measureCache.put(p, m);
+
+ assertThat(measureCache.contains(p, m)).isTrue();
+ assertThat(measureCache.entries()).hasSize(1);
+ Iterator<Entry<Measure>> iterator = measureCache.entries().iterator();
+ iterator.hasNext();
+ Entry<Measure> next = iterator.next();
+ assertThat(next.value()).isEqualTo(m);
+ assertThat(next.key()[0]).isEqualTo("struts");
+
+ assertThat(measureCache.byResource(p)).hasSize(1);
+ assertThat(measureCache.byResource(p).iterator().next()).isEqualTo(m);
+ }
+
+ /**
+ * This test fails with stock PersisitIt.
+ */
+ @Test
+ public void should_add_measure_with_too_big_data_for_persistit_pre_patch() {
+ Project p = new Project("struts");
+
+ assertThat(measureCache.entries()).hasSize(0);
+
+ assertThat(measureCache.byResource(p)).hasSize(0);
+
+ Measure m = new Measure(CoreMetrics.NCLOC, 1.0).setDate(new Date());
+ StringBuilder data = new StringBuilder();
+ for (int i = 0; i < 500000; i++) {
+ data.append("some data");
+ }
+ m.setData(data.toString());
+
+ measureCache.put(p, m);
+
+ assertThat(measureCache.contains(p, m)).isTrue();
+ assertThat(measureCache.entries()).hasSize(1);
+ Iterator<Entry<Measure>> iterator = measureCache.entries().iterator();
+ iterator.hasNext();
+ Entry<Measure> next = iterator.next();
+ assertThat(next.value()).isEqualTo(m);
+ assertThat(next.key()[0]).isEqualTo("struts");
+
+ assertThat(measureCache.byResource(p)).hasSize(1);
+ assertThat(measureCache.byResource(p).iterator().next()).isEqualTo(m);
+
+ }
+
+ @Test
+ public void should_add_measure_with_too_big_data_for_persistit() {
+ Project p = new Project("struts");
+
+ assertThat(measureCache.entries()).hasSize(0);
+
+ assertThat(measureCache.byResource(p)).hasSize(0);
+
+ Measure m = new Measure(CoreMetrics.NCLOC, 1.0).setDate(new Date());
+ StringBuilder data = new StringBuilder(64 * 1024 * 1024 + 1);
+ // Limit is 64Mo
+ for (int i = 0; i < (64 * 1024 * 1024 + 1); i++) {
+ data.append('a');
+ }
+ m.setData(data.toString());
+
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Fail to put element in the cache measures");
+
+ measureCache.put(p, m);
+ }
+
+ @Test
+ public void should_get_measures() {
+ Project p = new Project("struts");
+ Resource dir = Directory.create("foo/bar").setEffectiveKey("struts:foo/bar");
+ Resource file1 = Directory.create("foo/bar/File1.txt").setEffectiveKey("struts:foo/bar/File1.txt");
+ Resource file2 = Directory.create("foo/bar/File2.txt").setEffectiveKey("struts:foo/bar/File2.txt");
+
+ assertThat(measureCache.entries()).hasSize(0);
+
+ assertThat(measureCache.byResource(p)).hasSize(0);
+ assertThat(measureCache.byResource(dir)).hasSize(0);
+
+ Measure mFile1 = new Measure(CoreMetrics.NCLOC, 1.0);
+ measureCache.put(file1, mFile1);
+ Measure mFile2 = new Measure(CoreMetrics.NCLOC, 3.0);
+ measureCache.put(file2, mFile2);
+
+ assertThat(measureCache.entries()).hasSize(2);
+ assertThat(measureCache.byResource(p)).hasSize(0);
+ assertThat(measureCache.byResource(dir)).hasSize(0);
+
+ Measure mDir = new Measure(CoreMetrics.NCLOC, 4.0);
+ measureCache.put(dir, mDir);
+
+ assertThat(measureCache.entries()).hasSize(3);
+ assertThat(measureCache.byResource(p)).hasSize(0);
+ assertThat(measureCache.byResource(dir)).hasSize(1);
+ assertThat(measureCache.byResource(dir).iterator().next()).isEqualTo(mDir);
+
+ Measure mProj = new Measure(CoreMetrics.NCLOC, 4.0);
+ measureCache.put(p, mProj);
+
+ assertThat(measureCache.entries()).hasSize(4);
+ assertThat(measureCache.byResource(p)).hasSize(1);
+ assertThat(measureCache.byResource(p).iterator().next()).isEqualTo(mProj);
+ assertThat(measureCache.byResource(dir)).hasSize(1);
+ assertThat(measureCache.byResource(dir).iterator().next()).isEqualTo(mDir);
+ }
+
+ @Test
+ public void test_measure_coder() throws Exception {
+ Resource file1 = File.create("foo/bar/File1.txt").setEffectiveKey("struts:foo/bar/File1.txt");
+
+ Measure measure = new Measure(CoreMetrics.NCLOC, 3.14);
+ measure.setData("data");
+ measure.setAlertStatus(Level.ERROR);
+ measure.setAlertText("alert");
+ measure.setDate(new Date());
+ measure.setDescription("description");
+ measure.setPersistenceMode(null);
+ measure.setPersonId(3);
+ measure.setUrl("http://foo");
+ measure.setVariation1(11.0);
+ measure.setVariation2(12.0);
+ measure.setVariation3(13.0);
+ measure.setVariation4(14.0);
+ measure.setVariation5(15.0);
+ measureCache.put(file1, measure);
+
+ Measure savedMeasure = measureCache.byResource(file1).iterator().next();
+ assertThat(EqualsBuilder.reflectionEquals(measure, savedMeasure)).isTrue();
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/ConsoleReportTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/ConsoleReportTest.java
new file mode 100644
index 00000000000..1a49bb94731
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/ConsoleReportTest.java
@@ -0,0 +1,145 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import javax.annotation.Nullable;
+
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.config.Settings;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ConsoleReportTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private Settings settings;
+ private IssueCache issueCache;
+ private InputPathCache inputPathCache;
+ private ConsoleReport report;
+
+ @Before
+ public void prepare() {
+ settings = new Settings();
+ issueCache = mock(IssueCache.class);
+ inputPathCache = mock(InputPathCache.class);
+ report = new ConsoleReport(settings, issueCache, inputPathCache);
+ }
+
+ @Test
+ public void dontExecuteByDefault() {
+ report.execute();
+ for (String log : logTester.logs()) {
+ assertThat(log).doesNotContain(ConsoleReport.HEADER);
+ }
+ }
+
+ @Test
+ public void testNoFile() {
+ settings.setProperty(ConsoleReport.CONSOLE_REPORT_ENABLED_KEY, "true");
+ when(inputPathCache.allFiles()).thenReturn(Collections.<InputFile>emptyList());
+ when(issueCache.all()).thenReturn(Collections.<TrackedIssue>emptyList());
+ report.execute();
+ assertThat(getReportLog()).isEqualTo(
+ "\n\n------------- Issues Report -------------\n\n" +
+ " No file analyzed\n" +
+ "\n-------------------------------------------\n\n");
+ }
+
+ @Test
+ public void testNoNewIssue() {
+ settings.setProperty(ConsoleReport.CONSOLE_REPORT_ENABLED_KEY, "true");
+ when(inputPathCache.allFiles()).thenReturn(Arrays.<InputFile>asList(new DefaultInputFile("foo", "src/Foo.php")));
+ when(issueCache.all()).thenReturn(Arrays.asList(createIssue(false, null)));
+ report.execute();
+ assertThat(getReportLog()).isEqualTo(
+ "\n\n------------- Issues Report -------------\n\n" +
+ " No new issue\n" +
+ "\n-------------------------------------------\n\n");
+ }
+
+ @Test
+ public void testOneNewIssue() {
+ settings.setProperty(ConsoleReport.CONSOLE_REPORT_ENABLED_KEY, "true");
+ when(inputPathCache.allFiles()).thenReturn(Arrays.<InputFile>asList(new DefaultInputFile("foo", "src/Foo.php")));
+ when(issueCache.all()).thenReturn(Arrays.asList(createIssue(true, Severity.BLOCKER)));
+ report.execute();
+ assertThat(getReportLog()).isEqualTo(
+ "\n\n------------- Issues Report -------------\n\n" +
+ " +1 issue\n\n" +
+ " +1 blocker\n" +
+ "\n-------------------------------------------\n\n");
+ }
+
+ @Test
+ public void testOneNewIssuePerSeverity() {
+ settings.setProperty(ConsoleReport.CONSOLE_REPORT_ENABLED_KEY, "true");
+ when(inputPathCache.allFiles()).thenReturn(Arrays.<InputFile>asList(new DefaultInputFile("foo", "src/Foo.php")));
+ when(issueCache.all()).thenReturn(Arrays.asList(
+ createIssue(true, Severity.BLOCKER),
+ createIssue(true, Severity.CRITICAL),
+ createIssue(true, Severity.MAJOR),
+ createIssue(true, Severity.MINOR),
+ createIssue(true, Severity.INFO)));
+ report.execute();
+ assertThat(getReportLog()).isEqualTo(
+ "\n\n------------- Issues Report -------------\n\n" +
+ " +5 issues\n\n" +
+ " +1 blocker\n" +
+ " +1 critical\n" +
+ " +1 major\n" +
+ " +1 minor\n" +
+ " +1 info\n" +
+ "\n-------------------------------------------\n\n");
+ }
+
+ private String getReportLog() {
+ for (String log : logTester.logs()) {
+ if (log.contains(ConsoleReport.HEADER)) {
+ return log;
+ }
+ }
+ throw new IllegalStateException("No console report");
+ }
+
+ private TrackedIssue createIssue(boolean isNew, @Nullable String severity) {
+ TrackedIssue issue = new TrackedIssue();
+ issue.setNew(isNew);
+ issue.setSeverity(severity);
+
+ return issue;
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/JSONReportTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/JSONReportTest.java
new file mode 100644
index 00000000000..da798a2dbf1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/JSONReportTest.java
@@ -0,0 +1,174 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import com.google.common.collect.Lists;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.TimeZone;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputDir;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.rule.Rules;
+import org.sonar.api.batch.rule.internal.RulesBuilder;
+import org.sonar.api.config.Settings;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.platform.Server;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.issue.tracking.TrackedIssue;
+import org.sonar.batch.repository.user.UserRepositoryLoader;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+import org.sonar.scanner.protocol.input.ScannerInput;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssert.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class JSONReportTest {
+
+ private SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+
+ @org.junit.Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ JSONReport jsonReport;
+ Resource resource = mock(Resource.class);
+ DefaultFileSystem fs;
+ Server server = mock(Server.class);
+ Rules rules = mock(Rules.class);
+ Settings settings = new Settings();
+ IssueCache issueCache = mock(IssueCache.class);
+ private UserRepositoryLoader userRepository;
+
+ @Before
+ public void before() throws Exception {
+ fs = new DefaultFileSystem(temp.newFolder().toPath());
+ SIMPLE_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+02:00"));
+ when(resource.getEffectiveKey()).thenReturn("Action.java");
+ when(server.getVersion()).thenReturn("3.6");
+ userRepository = mock(UserRepositoryLoader.class);
+ DefaultInputDir inputDir = new DefaultInputDir("struts", "src/main/java/org/apache/struts");
+ DefaultInputFile inputFile = new DefaultInputFile("struts", "src/main/java/org/apache/struts/Action.java");
+ inputFile.setStatus(InputFile.Status.CHANGED);
+ InputPathCache fileCache = mock(InputPathCache.class);
+ when(fileCache.allFiles()).thenReturn(Arrays.<InputFile>asList(inputFile));
+ when(fileCache.allDirs()).thenReturn(Arrays.<InputDir>asList(inputDir));
+ Project rootModule = new Project("struts");
+ Project moduleA = new Project("struts-core");
+ moduleA.setParent(rootModule).setPath("core");
+ Project moduleB = new Project("struts-ui");
+ moduleB.setParent(rootModule).setPath("ui");
+
+ RulesBuilder builder = new RulesBuilder();
+ builder.add(RuleKey.of("squid", "AvoidCycles")).setName("Avoid Cycles");
+ rules = builder.build();
+ jsonReport = new JSONReport(settings, fs, server, rules, issueCache, rootModule, fileCache, userRepository);
+ }
+
+ @Test
+ public void should_write_json() throws Exception {
+ TrackedIssue issue = new TrackedIssue();
+ issue.setKey("200");
+ issue.setComponentKey("struts:src/main/java/org/apache/struts/Action.java");
+ issue.setRuleKey(RuleKey.of("squid", "AvoidCycles"));
+ issue.setMessage("There are 2 cycles");
+ issue.setSeverity("MINOR");
+ issue.setStatus(Issue.STATUS_OPEN);
+ issue.setResolution(null);
+ issue.setStartLine(1);
+ issue.setEndLine(2);
+ issue.setStartLineOffset(3);
+ issue.setEndLineOffset(4);
+ issue.setGap(3.14);
+ issue.setReporter("julien");
+ issue.setAssignee("simon");
+ issue.setCreationDate(SIMPLE_DATE_FORMAT.parse("2013-04-24"));
+ issue.setNew(false);
+ when(issueCache.all()).thenReturn(Lists.newArrayList(issue));
+ ScannerInput.User user1 = ScannerInput.User.newBuilder().setLogin("julien").setName("Julien").build();
+ ScannerInput.User user2 = ScannerInput.User.newBuilder().setLogin("simon").setName("Simon").build();
+ when(userRepository.load("julien")).thenReturn(user1);
+ when(userRepository.load("simon")).thenReturn(user2);
+
+ StringWriter writer = new StringWriter();
+ jsonReport.writeJson(writer);
+
+ assertThatJson(writer.toString()).isEqualTo(IOUtils.toString(this.getClass().getResource(this.getClass().getSimpleName() + "/report.json")));
+ }
+
+ @Test
+ public void should_exclude_resolved_issues() throws Exception {
+ RuleKey ruleKey = RuleKey.of("squid", "AvoidCycles");
+ TrackedIssue issue = new TrackedIssue();
+ issue.setKey("200");
+ issue.setComponentKey("struts:src/main/java/org/apache/struts/Action.java");
+ issue.setRuleKey(ruleKey);
+ issue.setStatus(Issue.STATUS_CLOSED);
+ issue.setResolution(Issue.RESOLUTION_FIXED);
+ issue.setCreationDate(SIMPLE_DATE_FORMAT.parse("2013-04-24"));
+ issue.setNew(false);
+ when(issueCache.all()).thenReturn(Lists.newArrayList(issue));
+
+ StringWriter writer = new StringWriter();
+ jsonReport.writeJson(writer);
+
+ assertThatJson(writer.toString()).isEqualTo(IOUtils.toString(this.getClass().getResource(this.getClass().getSimpleName() + "/report-without-resolved-issues.json")));
+ }
+
+ @Test
+ public void should_not_export_by_default() throws IOException {
+ File workDir = temp.newFolder("sonar");
+ fs.setWorkDir(workDir);
+
+ jsonReport.execute();
+
+ verifyZeroInteractions(issueCache);
+ }
+
+ @Test
+ public void should_export_issues_to_file() throws IOException {
+ File workDir = temp.newFolder("sonar");
+ fs.setWorkDir(workDir);
+
+ when(issueCache.all()).thenReturn(Collections.<TrackedIssue>emptyList());
+
+ settings.setProperty("sonar.report.export.path", "output.json");
+
+ jsonReport.execute();
+
+ assertThat(new File(workDir, "output.json")).exists();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/RuleNameProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/RuleNameProviderTest.java
new file mode 100644
index 00000000000..3c3af0e689c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scan/report/RuleNameProviderTest.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scan.report;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import static org.mockito.Matchers.any;
+
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.batch.rule.Rule;
+import org.junit.Test;
+import org.junit.Before;
+import org.sonar.api.batch.rule.Rules;
+
+public class RuleNameProviderTest {
+ RuleNameProvider provider;
+ Rules rules;
+ Rule rule;
+ RuleKey ruleKey;
+
+ @Before
+ public void setUp() {
+ ruleKey = mock(RuleKey.class);
+ rule = mock(Rule.class);
+ rules = mock(Rules.class);
+ provider = new RuleNameProvider(rules);
+
+ when(ruleKey.rule()).thenReturn("ruleKey");
+ when(ruleKey.repository()).thenReturn("repoKey");
+
+ when(rule.name()).thenReturn("name");
+ when(rule.key()).thenReturn(ruleKey);
+
+ when(rules.find(any(RuleKey.class))).thenReturn(rule);
+ }
+
+ @Test
+ public void testNameForHTML() {
+ assertThat(provider.nameForHTML(rule)).isEqualTo(rule.name());
+ assertThat(provider.nameForHTML(ruleKey)).isEqualTo(rule.name());
+ }
+
+ @Test
+ public void testNameForJS() {
+ assertThat(provider.nameForJS("repoKey:ruleKey")).isEqualTo(rule.name());
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/scm/DefaultBlameOutputTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/scm/DefaultBlameOutputTest.java
new file mode 100644
index 00000000000..743f9175ba4
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/scm/DefaultBlameOutputTest.java
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.scm;
+
+import java.util.Arrays;
+import java.util.Date;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.scm.BlameLine;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.index.BatchComponentCache;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultBlameOutputTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private BatchComponentCache componentCache;
+
+ @Before
+ public void prepare() {
+ componentCache = mock(BatchComponentCache.class);
+ BatchComponent component = mock(BatchComponent.class);
+ when(component.batchId()).thenReturn(1);
+ when(componentCache.get(any(InputComponent.class))).thenReturn(component);
+ }
+
+ @Test
+ public void shouldNotFailIfNotSameNumberOfLines() {
+ InputFile file = new DefaultInputFile("foo", "src/main/java/Foo.java").setLines(10);
+
+ new DefaultBlameOutput(null, null, Arrays.asList(file)).blameResult(file, Arrays.asList(new BlameLine().revision("1").author("guy")));
+ }
+
+ @Test
+ public void shouldFailIfNotExpectedFile() {
+ InputFile file = new DefaultInputFile("foo", "src/main/java/Foo.java").setLines(1);
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("It was not expected to blame file src/main/java/Foo.java");
+
+ new DefaultBlameOutput(null, null, Arrays.<InputFile>asList(new DefaultInputFile("foo", "src/main/java/Foo2.java")))
+ .blameResult(file, Arrays.asList(new BlameLine().revision("1").author("guy")));
+ }
+
+ @Test
+ public void shouldFailIfNullDate() {
+ InputFile file = new DefaultInputFile("foo", "src/main/java/Foo.java").setLines(1);
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Blame date is null for file src/main/java/Foo.java at line 1");
+
+ new DefaultBlameOutput(null, componentCache, Arrays.<InputFile>asList(file))
+ .blameResult(file, Arrays.asList(new BlameLine().revision("1").author("guy")));
+ }
+
+ @Test
+ public void shouldFailIfNullRevision() {
+ InputFile file = new DefaultInputFile("foo", "src/main/java/Foo.java").setLines(1);
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Blame revision is blank for file src/main/java/Foo.java at line 1");
+
+ new DefaultBlameOutput(null, componentCache, Arrays.<InputFile>asList(file))
+ .blameResult(file, Arrays.asList(new BlameLine().date(new Date()).author("guy")));
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java
new file mode 100644
index 00000000000..ba40858840d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorContextTest.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor;
+
+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.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.InputModule;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultSensorContextTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private ActiveRules activeRules;
+ private DefaultFileSystem fs;
+ private DefaultSensorContext adaptor;
+ private Settings settings;
+ private SensorStorage sensorStorage;
+ private AnalysisMode analysisMode;
+
+ @Before
+ public void prepare() throws Exception {
+ activeRules = new ActiveRulesBuilder().build();
+ fs = new DefaultFileSystem(temp.newFolder().toPath());
+ MetricFinder metricFinder = mock(MetricFinder.class);
+ when(metricFinder.findByKey(CoreMetrics.NCLOC_KEY)).thenReturn(CoreMetrics.NCLOC);
+ when(metricFinder.findByKey(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)).thenReturn(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION);
+ settings = new Settings();
+ sensorStorage = mock(SensorStorage.class);
+ analysisMode = mock(AnalysisMode.class);
+ adaptor = new DefaultSensorContext(mock(InputModule.class), settings, fs, activeRules, analysisMode, sensorStorage);
+ }
+
+ @Test
+ public void shouldProvideComponents() {
+ assertThat(adaptor.activeRules()).isEqualTo(activeRules);
+ assertThat(adaptor.fileSystem()).isEqualTo(fs);
+ assertThat(adaptor.settings()).isEqualTo(settings);
+
+ assertThat(adaptor.newIssue()).isNotNull();
+ assertThat(adaptor.newMeasure()).isNotNull();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorStorageTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorStorageTest.java
new file mode 100644
index 00000000000..caa16b11a19
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/DefaultSensorStorageTest.java
@@ -0,0 +1,140 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.batch.cpd.index.SonarCpdBlockIndex;
+import org.sonar.batch.index.BatchComponentCache;
+import org.sonar.batch.issue.ModuleIssues;
+import org.sonar.batch.report.ReportPublisher;
+import org.sonar.batch.scan.measure.MeasureCache;
+import org.sonar.batch.sensor.coverage.CoverageExclusions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultSensorStorageTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private ActiveRules activeRules;
+ private DefaultFileSystem fs;
+ private DefaultSensorStorage sensorStorage;
+ private Settings settings;
+ private ModuleIssues moduleIssues;
+ private Project project;
+ private MeasureCache measureCache;
+
+ private BatchComponentCache resourceCache;
+
+ @Before
+ public void prepare() throws Exception {
+ activeRules = new ActiveRulesBuilder().build();
+ fs = new DefaultFileSystem(temp.newFolder().toPath());
+ MetricFinder metricFinder = mock(MetricFinder.class);
+ when(metricFinder.findByKey(CoreMetrics.NCLOC_KEY)).thenReturn(CoreMetrics.NCLOC);
+ when(metricFinder.findByKey(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)).thenReturn(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION);
+ settings = new Settings();
+ moduleIssues = mock(ModuleIssues.class);
+ project = new Project("myProject");
+ measureCache = mock(MeasureCache.class);
+ CoverageExclusions coverageExclusions = mock(CoverageExclusions.class);
+ when(coverageExclusions.accept(any(Resource.class), any(Measure.class))).thenReturn(true);
+ resourceCache = new BatchComponentCache();
+ sensorStorage = new DefaultSensorStorage(metricFinder,
+ moduleIssues, settings, fs, activeRules, coverageExclusions, resourceCache, mock(ReportPublisher.class), measureCache, mock(SonarCpdBlockIndex.class));
+ }
+
+ @Test
+ public void shouldFailIfUnknowMetric() {
+ InputFile file = new DefaultInputFile("foo", "src/Foo.php");
+
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Unknow metric with key: lines");
+
+ sensorStorage.store(new DefaultMeasure()
+ .on(file)
+ .forMetric(CoreMetrics.LINES)
+ .withValue(10));
+ }
+
+ @Test
+ public void shouldSaveFileMeasureToSensorContext() {
+ InputFile file = new DefaultInputFile("foo", "src/Foo.php");
+
+ ArgumentCaptor<org.sonar.api.measures.Measure> argumentCaptor = ArgumentCaptor.forClass(org.sonar.api.measures.Measure.class);
+ Resource sonarFile = File.create("src/Foo.php").setEffectiveKey("foo:src/Foo.php");
+ resourceCache.add(sonarFile, null).setInputComponent(file);
+ when(measureCache.put(eq(sonarFile), argumentCaptor.capture())).thenReturn(null);
+ sensorStorage.store(new DefaultMeasure()
+ .on(file)
+ .forMetric(CoreMetrics.NCLOC)
+ .withValue(10));
+
+ org.sonar.api.measures.Measure m = argumentCaptor.getValue();
+ assertThat(m.getValue()).isEqualTo(10.0);
+ assertThat(m.getMetric()).isEqualTo(CoreMetrics.NCLOC);
+ }
+
+ @Test
+ public void shouldSaveProjectMeasureToSensorContext() {
+ DefaultInputModule module = new DefaultInputModule(project.getEffectiveKey());
+ resourceCache.add(project, null).setInputComponent(module);
+
+ ArgumentCaptor<org.sonar.api.measures.Measure> argumentCaptor = ArgumentCaptor.forClass(org.sonar.api.measures.Measure.class);
+ when(measureCache.put(eq(project), argumentCaptor.capture())).thenReturn(null);
+
+ sensorStorage.store(new DefaultMeasure()
+ .on(module)
+ .forMetric(CoreMetrics.NCLOC)
+ .withValue(10));
+
+ org.sonar.api.measures.Measure m = argumentCaptor.getValue();
+ assertThat(m.getValue()).isEqualTo(10.0);
+ assertThat(m.getMetric()).isEqualTo(CoreMetrics.NCLOC);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/SensorOptimizerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/SensorOptimizerTest.java
new file mode 100644
index 00000000000..0fac9a72139
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/SensorOptimizerTest.java
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor;
+
+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.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor;
+import org.sonar.api.config.Settings;
+import org.sonar.api.rule.RuleKey;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SensorOptimizerTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private DefaultFileSystem fs;
+ private SensorOptimizer optimizer;
+ private Settings settings;
+
+ @Before
+ public void prepare() throws Exception {
+ fs = new DefaultFileSystem(temp.newFolder().toPath());
+ settings = new Settings();
+ optimizer = new SensorOptimizer(fs, new ActiveRulesBuilder().build(), settings);
+ }
+
+ @Test
+ public void should_run_analyzer_with_no_metadata() {
+ DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor();
+
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+ @Test
+ public void should_optimize_on_language() {
+ DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor()
+ .onlyOnLanguages("java", "php");
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ fs.add(new DefaultInputFile("foo", "src/Foo.java").setLanguage("java"));
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+ @Test
+ public void should_optimize_on_type() {
+ DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor()
+ .onlyOnFileType(InputFile.Type.MAIN);
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ fs.add(new DefaultInputFile("foo", "tests/FooTest.java").setType(InputFile.Type.TEST));
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ fs.add(new DefaultInputFile("foo", "src/Foo.java").setType(InputFile.Type.MAIN));
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+ @Test
+ public void should_optimize_on_both_type_and_language() {
+ DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor()
+ .onlyOnLanguages("java", "php")
+ .onlyOnFileType(InputFile.Type.MAIN);
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ fs.add(new DefaultInputFile("foo", "tests/FooTest.java").setLanguage("java").setType(InputFile.Type.TEST));
+ fs.add(new DefaultInputFile("foo", "src/Foo.cbl").setLanguage("cobol").setType(InputFile.Type.MAIN));
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ fs.add(new DefaultInputFile("foo", "src/Foo.java").setLanguage("java").setType(InputFile.Type.MAIN));
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+ @Test
+ public void should_optimize_on_repository() {
+ DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor()
+ .createIssuesForRuleRepositories("squid");
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ ActiveRules activeRules = new ActiveRulesBuilder()
+ .create(RuleKey.of("repo1", "foo"))
+ .activate()
+ .build();
+ optimizer = new SensorOptimizer(fs, activeRules, settings);
+
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ activeRules = new ActiveRulesBuilder()
+ .create(RuleKey.of("repo1", "foo"))
+ .activate()
+ .create(RuleKey.of("squid", "rule"))
+ .activate()
+ .build();
+ optimizer = new SensorOptimizer(fs, activeRules, settings);
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+ @Test
+ public void should_optimize_on_settings() {
+ DefaultSensorDescriptor descriptor = new DefaultSensorDescriptor()
+ .requireProperty("sonar.foo.reportPath");
+ assertThat(optimizer.shouldExecute(descriptor)).isFalse();
+
+ settings.setProperty("sonar.foo.reportPath", "foo");
+ assertThat(optimizer.shouldExecute(descriptor)).isTrue();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/coverage/CoverageExclusionsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/coverage/CoverageExclusionsTest.java
new file mode 100644
index 00000000000..284226f9af0
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/sensor/coverage/CoverageExclusionsTest.java
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.sensor.coverage;
+
+import org.junit.rules.TemporaryFolder;
+
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.core.config.ExclusionProperties;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class CoverageExclusionsTest {
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private Settings settings;
+ private DefaultFileSystem fs;
+
+ private CoverageExclusions filter;
+
+ @Before
+ public void createFilter() {
+ settings = new Settings(new PropertyDefinitions(ExclusionProperties.all()));
+ fs = new DefaultFileSystem(temp.getRoot());
+ filter = new CoverageExclusions(settings, fs);
+ }
+
+ @Test
+ public void shouldValidateStrictlyPositiveLine() {
+ DefaultInputFile file = new DefaultInputFile("module", "testfile");
+ Measure measure = mock(Measure.class);
+ Map<Integer, Integer> map = ImmutableMap.of(0, 3);
+
+ String data = KeyValueFormat.format(map);
+ when(measure.getMetric()).thenReturn(CoreMetrics.IT_CONDITIONS_BY_LINE);
+ when(measure.getData()).thenReturn(data);
+
+ fs.add(file);
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("must be > 0");
+ filter.validate(measure, "testfile");
+ }
+
+ @Test
+ public void shouldValidateFileExists() {
+ DefaultInputFile file = new DefaultInputFile("module", "testfile");
+ Measure measure = mock(Measure.class);
+ Map<Integer, Integer> map = ImmutableMap.of(0, 3);
+
+ String data = KeyValueFormat.format(map);
+ when(measure.getMetric()).thenReturn(CoreMetrics.IT_CONDITIONS_BY_LINE);
+ when(measure.getData()).thenReturn(data);
+
+ fs.add(file);
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("resource is not indexed as a file");
+ filter.validate(measure, "dummy");
+ }
+
+ @Test
+ public void shouldValidateMaxLine() {
+ DefaultInputFile file = new DefaultInputFile("module", "testfile");
+ file.setLines(10);
+ Measure measure = mock(Measure.class);
+ Map<Integer, Integer> map = ImmutableMap.of(11, 3);
+
+ String data = KeyValueFormat.format(map);
+ when(measure.getMetric()).thenReturn(CoreMetrics.COVERED_CONDITIONS_BY_LINE);
+ when(measure.getData()).thenReturn(data);
+
+ exception.expect(IllegalStateException.class);
+ filter.validate(measure, file);
+ }
+
+ @Test
+ public void shouldNotFilterNonCoverageMetrics() {
+ Measure otherMeasure = mock(Measure.class);
+ when(otherMeasure.getMetric()).thenReturn(CoreMetrics.LINES);
+ assertThat(filter.accept(mock(Resource.class), otherMeasure)).isTrue();
+ }
+
+ @Test
+ public void shouldFilterFileBasedOnPattern() {
+ Resource resource = File.create("src/org/polop/File.php", null, false);
+ Measure coverageMeasure = mock(Measure.class);
+ when(coverageMeasure.getMetric()).thenReturn(CoreMetrics.LINES_TO_COVER);
+
+ settings.setProperty("sonar.coverage.exclusions", "src/org/polop/*");
+ filter.initPatterns();
+ assertThat(filter.accept(resource, coverageMeasure)).isFalse();
+ }
+
+ @Test
+ public void shouldNotFilterFileBasedOnPattern() {
+ Resource resource = File.create("src/org/polop/File.php", null, false);
+ Measure coverageMeasure = mock(Measure.class);
+ when(coverageMeasure.getMetric()).thenReturn(CoreMetrics.COVERAGE);
+
+ settings.setProperty("sonar.coverage.exclusions", "src/org/other/*");
+ filter.initPatterns();
+ assertThat(filter.accept(resource, coverageMeasure)).isTrue();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/source/CodeColorizersTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/CodeColorizersTest.java
new file mode 100644
index 00000000000..2a9530e1142
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/CodeColorizersTest.java
@@ -0,0 +1,230 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.web.CodeColorizerFormat;
+import org.sonar.colorizer.CDocTokenizer;
+import org.sonar.colorizer.CppDocTokenizer;
+import org.sonar.colorizer.JavadocTokenizer;
+import org.sonar.colorizer.KeywordsTokenizer;
+import org.sonar.colorizer.MultilinesDocTokenizer;
+import org.sonar.colorizer.RegexpTokenizer;
+import org.sonar.colorizer.StringTokenizer;
+import org.sonar.colorizer.Tokenizer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class CodeColorizersTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void testConvertToHighlighting() throws Exception {
+ CodeColorizers codeColorizers = new CodeColorizers(Arrays.<CodeColorizerFormat>asList(new JavaScriptColorizerFormat(), new WebCodeColorizerFormat()));
+ File jsFile = new File(this.getClass().getResource("CodeColorizersTest/Person.js").toURI());
+ NewHighlighting highlighting = mock(NewHighlighting.class);
+
+ codeColorizers.toSyntaxHighlighting(jsFile, StandardCharsets.UTF_8, "js", highlighting);
+
+ verifyForJs(highlighting);
+ }
+
+ private void verifyForJs(NewHighlighting highlighting) {
+ verify(highlighting).highlight(0, 4, TypeOfText.CPP_DOC);
+ verify(highlighting).highlight(5, 11, TypeOfText.CPP_DOC);
+ verify(highlighting).highlight(12, 15, TypeOfText.CPP_DOC);
+ verify(highlighting).highlight(16, 19, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(29, 37, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(65, 69, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(85, 93, TypeOfText.COMMENT);
+ verify(highlighting).highlight(98, 102, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(112, 114, TypeOfText.STRING);
+ verify(highlighting).highlight(120, 124, TypeOfText.KEYWORD);
+ }
+
+ @Test
+ public void testConvertToHighlightingIgnoreBOM() throws Exception {
+ CodeColorizers codeColorizers = new CodeColorizers(Arrays.<CodeColorizerFormat>asList(new JavaScriptColorizerFormat(), new WebCodeColorizerFormat()));
+
+ File fileWithBom = temp.newFile();
+ FileUtils.write(fileWithBom, "\uFEFF", "UTF-8");
+ File jsFile = new File(this.getClass().getResource("CodeColorizersTest/Person.js").toURI());
+ FileUtils.write(fileWithBom, FileUtils.readFileToString(jsFile), "UTF-8", true);
+
+ NewHighlighting highlighting = mock(NewHighlighting.class);
+ codeColorizers.toSyntaxHighlighting(fileWithBom, StandardCharsets.UTF_8, "js", highlighting);
+
+ verifyForJs(highlighting);
+ }
+
+ @Test
+ public void shouldSupportJavaIfNotProvidedByJavaPluginForBackwardCompatibility() throws Exception {
+ CodeColorizers codeColorizers = new CodeColorizers(Arrays.<CodeColorizerFormat>asList());
+
+ File javaFile = new File(this.getClass().getResource("CodeColorizersTest/Person.java").toURI());
+
+ NewHighlighting highlighting = mock(NewHighlighting.class);
+ codeColorizers.toSyntaxHighlighting(javaFile, StandardCharsets.UTF_8, "java", highlighting);
+
+ verify(highlighting).highlight(0, 4, TypeOfText.STRUCTURED_COMMENT);
+ verify(highlighting).highlight(5, 11, TypeOfText.STRUCTURED_COMMENT);
+ verify(highlighting).highlight(12, 15, TypeOfText.STRUCTURED_COMMENT);
+ verify(highlighting).highlight(16, 22, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(23, 28, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(43, 50, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(51, 54, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(67, 78, TypeOfText.ANNOTATION);
+ verify(highlighting).highlight(81, 87, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(88, 92, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(97, 100, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(142, 146, TypeOfText.KEYWORD);
+ verify(highlighting).highlight(162, 170, TypeOfText.COMMENT);
+ }
+
+ @Test
+ public void testConvertHtmlToHighlightingWithMacEoL() throws Exception {
+ CodeColorizers codeColorizers = new CodeColorizers(Arrays.<CodeColorizerFormat>asList(new JavaScriptColorizerFormat(), new WebCodeColorizerFormat()));
+ File htmlFile = new File(this.getClass().getResource("CodeColorizersTest/package.html").toURI());
+ SensorStorage sensorStorage = mock(SensorStorage.class);
+ DefaultHighlighting highlighting = new DefaultHighlighting(sensorStorage);
+ highlighting.onFile(new DefaultInputFile("FOO", "package.html")
+ .initMetadata(new FileMetadata().readMetadata(htmlFile, StandardCharsets.UTF_8)));
+
+ codeColorizers.toSyntaxHighlighting(htmlFile, StandardCharsets.UTF_8, "web", highlighting);
+
+ assertThat(highlighting.getSyntaxHighlightingRuleSet()).extracting("range.start.line", "range.start.lineOffset", "range.end.line", "range.end.lineOffset", "textType")
+ .containsExactly(
+ tuple(1, 0, 1, 132, TypeOfText.STRUCTURED_COMMENT),
+ tuple(2, 0, 2, 6, TypeOfText.KEYWORD),
+ tuple(3, 0, 3, 3, TypeOfText.KEYWORD),
+ tuple(4, 0, 4, 3, TypeOfText.KEYWORD),
+ // SONARWEB-26
+ tuple(5, 42, 12, 0, TypeOfText.STRING));
+ }
+
+ public static class JavaScriptColorizerFormat extends CodeColorizerFormat {
+
+ public JavaScriptColorizerFormat() {
+ super("js");
+ }
+
+ @Override
+ public List<Tokenizer> getTokenizers() {
+ return ImmutableList.<Tokenizer>of(
+ new StringTokenizer("<span class=\"s\">", "</span>"),
+ new CDocTokenizer("<span class=\"cd\">", "</span>"),
+ new JavadocTokenizer("<span class=\"cppd\">", "</span>"),
+ new CppDocTokenizer("<span class=\"cppd\">", "</span>"),
+ new KeywordsTokenizer("<span class=\"k\">", "</span>", "null",
+ "true",
+ "false",
+ "break",
+ "case",
+ "catch",
+ "class",
+ "continue",
+ "debugger",
+ "default",
+ "delete",
+ "do",
+ "extends",
+ "else",
+ "finally",
+ "for",
+ "function",
+ "if",
+ "import",
+ "in",
+ "instanceof",
+ "new",
+ "return",
+ "super",
+ "switch",
+ "this",
+ "throw",
+ "try",
+ "typeof",
+ "var",
+ "void",
+ "while",
+ "with",
+ "yield",
+ "const",
+ "enum",
+ "export"));
+ }
+
+ }
+
+ public class WebCodeColorizerFormat extends CodeColorizerFormat {
+
+ private final List<Tokenizer> tokenizers = new ArrayList<>();
+
+ public WebCodeColorizerFormat() {
+ super("web");
+ String tagAfter = "</span>";
+
+ // == tags ==
+ tokenizers.add(new RegexpTokenizer("<span class=\"k\">", tagAfter, "</?[:\\w]+>?"));
+ tokenizers.add(new RegexpTokenizer("<span class=\"k\">", tagAfter, ">"));
+
+ // == doctype ==
+ tokenizers.add(new RegexpTokenizer("<span class=\"j\">", tagAfter, "<!DOCTYPE.*>"));
+
+ // == comments ==
+ tokenizers.add(new MultilinesDocTokenizer("<!--", "-->", "<span class=\"j\">", tagAfter));
+ tokenizers.add(new MultilinesDocTokenizer("<%--", "--%>", "<span class=\"j\">", tagAfter));
+
+ // == expressions ==
+ tokenizers.add(new MultilinesDocTokenizer("<%@", "%>", "<span class=\"a\">", tagAfter));
+ tokenizers.add(new MultilinesDocTokenizer("<%", "%>", "<span class=\"a\">", tagAfter));
+
+ // == tag properties ==
+ tokenizers.add(new StringTokenizer("<span class=\"s\">", tagAfter));
+ }
+
+ @Override
+ public List<Tokenizer> getTokenizers() {
+ return tokenizers;
+ }
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultHighlightableTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultHighlightableTest.java
new file mode 100644
index 00000000000..3a791783b1d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultHighlightableTest.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import java.io.StringReader;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class DefaultHighlightableTest {
+
+ @Rule
+ public ExpectedException throwable = ExpectedException.none();
+
+ @Test
+ public void should_store_highlighting_rules() {
+ SensorStorage sensorStorage = mock(SensorStorage.class);
+ DefaultInputFile inputFile = new DefaultInputFile("foo", "src/Foo.php")
+ .initMetadata(new FileMetadata().readMetadata(new StringReader("azerty\nbla bla")));
+ DefaultHighlightable highlightablePerspective = new DefaultHighlightable(inputFile, sensorStorage, mock(AnalysisMode.class));
+ highlightablePerspective.newHighlighting().highlight(0, 6, "k").highlight(7, 10, "cppd").done();
+
+ ArgumentCaptor<DefaultHighlighting> argCaptor = ArgumentCaptor.forClass(DefaultHighlighting.class);
+ verify(sensorStorage).store(argCaptor.capture());
+ assertThat(argCaptor.getValue().getSyntaxHighlightingRuleSet()).hasSize(2);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolTableTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolTableTest.java
new file mode 100644
index 00000000000..86c7b7b316d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolTableTest.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.sonar.api.batch.fs.TextRange;
+import com.google.common.base.Strings;
+
+import java.io.StringReader;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.source.Symbol;
+import org.sonar.api.source.Symbolizable;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DefaultSymbolTableTest {
+
+ @Rule
+ public ExpectedException throwable = ExpectedException.none();
+ private DefaultInputFile inputFile;
+
+ @Before
+ public void prepare() {
+ inputFile = new DefaultInputFile("foo", "src/Foo.php")
+ .initMetadata(new FileMetadata().readMetadata(new StringReader(Strings.repeat("azerty\n", 20))));
+ }
+
+ @Test
+ public void should_order_symbol_and_references() {
+
+ Symbolizable.SymbolTableBuilder symbolTableBuilder = new DefaultSymbolTable.Builder(inputFile);
+ Symbol firstSymbol = symbolTableBuilder.newSymbol(10, 20);
+ symbolTableBuilder.newReference(firstSymbol, 32);
+ Symbol secondSymbol = symbolTableBuilder.newSymbol(84, 92);
+ symbolTableBuilder.newReference(secondSymbol, 124);
+ Symbol thirdSymbol = symbolTableBuilder.newSymbol(55, 62);
+ symbolTableBuilder.newReference(thirdSymbol, 70);
+ Symbolizable.SymbolTable symbolTable = symbolTableBuilder.build();
+
+ assertThat(symbolTable.symbols()).containsExactly(firstSymbol, secondSymbol, thirdSymbol);
+ }
+
+ @Test
+ public void variable_length_references() {
+ Symbolizable.SymbolTableBuilder symbolTableBuilder = new DefaultSymbolTable.Builder(inputFile);
+ Symbol firstSymbol = symbolTableBuilder.newSymbol(10, 20);
+ symbolTableBuilder.newReference(firstSymbol, 32);
+ symbolTableBuilder.newReference(firstSymbol, 44, 47);
+
+ DefaultSymbolTable symbolTable = (DefaultSymbolTable) symbolTableBuilder.build();
+
+ assertThat(symbolTable.symbols()).containsExactly(firstSymbol);
+
+ Set<TextRange> references = symbolTable.getReferencesBySymbol().get(firstSymbol);
+ assertThat(references).containsExactly(range(32, 42), range(44, 47));
+ }
+
+ private TextRange range(int start, int end) {
+ return inputFile.newRange(start, end);
+ }
+
+ @Test
+ public void should_reject_reference_conflicting_with_declaration() {
+ throwable.expect(UnsupportedOperationException.class);
+
+ Symbolizable.SymbolTableBuilder symbolTableBuilder = new DefaultSymbolTable.Builder(inputFile);
+ Symbol symbol = symbolTableBuilder.newSymbol(10, 20);
+ symbolTableBuilder.newReference(symbol, 15);
+ }
+
+ @Test
+ public void test_toString() throws Exception {
+ Symbolizable.SymbolTableBuilder symbolTableBuilder = new DefaultSymbolTable.Builder(inputFile);
+ Symbol symbol = symbolTableBuilder.newSymbol(10, 20);
+
+ assertThat(symbol.toString()).isEqualTo("Symbol{range=Range[from [line=2, lineOffset=3] to [line=3, lineOffset=6]]}");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolizableTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolizableTest.java
new file mode 100644
index 00000000000..f19a4b04099
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/DefaultSymbolizableTest.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import com.google.common.base.Strings;
+import java.io.StringReader;
+import java.util.Map;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.FileMetadata;
+import org.sonar.api.source.Symbol;
+import org.sonar.api.source.Symbolizable;
+import org.sonar.batch.sensor.DefaultSensorStorage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class DefaultSymbolizableTest {
+
+ @Test
+ public void should_update_cache_when_done() {
+
+ DefaultSensorStorage sensorStorage = mock(DefaultSensorStorage.class);
+ DefaultInputFile inputFile = new DefaultInputFile("foo", "src/Foo.php")
+ .initMetadata(new FileMetadata().readMetadata(new StringReader(Strings.repeat("azerty\n", 20))));
+
+ DefaultSymbolizable symbolPerspective = new DefaultSymbolizable(inputFile, sensorStorage, mock(AnalysisMode.class));
+ Symbolizable.SymbolTableBuilder symbolTableBuilder = symbolPerspective.newSymbolTableBuilder();
+ Symbol firstSymbol = symbolTableBuilder.newSymbol(4, 8);
+ symbolTableBuilder.newReference(firstSymbol, 12);
+ symbolTableBuilder.newReference(firstSymbol, 70);
+ Symbol otherSymbol = symbolTableBuilder.newSymbol(25, 33);
+ symbolTableBuilder.newReference(otherSymbol, 44);
+ symbolTableBuilder.newReference(otherSymbol, 60);
+ symbolTableBuilder.newReference(otherSymbol, 108);
+ Symbolizable.SymbolTable symbolTable = symbolTableBuilder.build();
+
+ symbolPerspective.setSymbolTable(symbolTable);
+
+ ArgumentCaptor<Map> argCaptor = ArgumentCaptor.forClass(Map.class);
+ verify(sensorStorage).store(eq(inputFile), argCaptor.capture());
+ // Map<Symbol, Set<TextRange>>
+ assertThat(argCaptor.getValue().keySet()).hasSize(2);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/source/HighlightableBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/HighlightableBuilderTest.java
new file mode 100644
index 00000000000..07ca60048b3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/HighlightableBuilderTest.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.junit.Test;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.source.Highlightable;
+import org.sonar.batch.index.BatchComponent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class HighlightableBuilderTest {
+
+ @Test
+ public void should_load_default_perspective() {
+ Resource file = File.create("foo.c").setEffectiveKey("myproject:path/to/foo.c");
+ BatchComponent component = new BatchComponent(1, file, null).setInputComponent(new DefaultInputFile("foo", "foo.c"));
+
+ HighlightableBuilder builder = new HighlightableBuilder(mock(SensorStorage.class), mock(AnalysisMode.class));
+ Highlightable perspective = builder.loadPerspective(Highlightable.class, component);
+
+ assertThat(perspective).isNotNull().isInstanceOf(DefaultHighlightable.class);
+ }
+
+ @Test
+ public void project_should_not_be_highlightable() {
+ BatchComponent component = new BatchComponent(1, new Project("struts").setEffectiveKey("org.struts"), null).setInputComponent(new DefaultInputModule("struts"));
+
+ HighlightableBuilder builder = new HighlightableBuilder(mock(SensorStorage.class), mock(AnalysisMode.class));
+ Highlightable perspective = builder.loadPerspective(Highlightable.class, component);
+
+ assertThat(perspective).isNull();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/source/SymbolizableBuilderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/SymbolizableBuilderTest.java
new file mode 100644
index 00000000000..5cc7fe2d06b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/source/SymbolizableBuilderTest.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.source;
+
+import org.junit.Test;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.component.Perspective;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.source.Symbolizable;
+import org.sonar.batch.index.BatchComponent;
+import org.sonar.batch.sensor.DefaultSensorStorage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class SymbolizableBuilderTest {
+
+ @Test
+ public void should_load_perspective() {
+ Resource file = File.create("foo.c").setEffectiveKey("myproject:path/to/foo.c");
+ BatchComponent component = new BatchComponent(1, file, null).setInputComponent(new DefaultInputFile("foo", "foo.c"));
+
+ SymbolizableBuilder perspectiveBuilder = new SymbolizableBuilder(mock(DefaultSensorStorage.class), mock(AnalysisMode.class));
+ Perspective perspective = perspectiveBuilder.loadPerspective(Symbolizable.class, component);
+
+ assertThat(perspective).isInstanceOf(Symbolizable.class);
+ }
+
+ @Test
+ public void project_should_not_be_highlightable() {
+ BatchComponent component = new BatchComponent(1, new Project("struts").setEffectiveKey("org.struts"), null).setInputComponent(new DefaultInputModule("struts"));
+
+ SymbolizableBuilder builder = new SymbolizableBuilder(mock(DefaultSensorStorage.class), mock(AnalysisMode.class));
+ Perspective perspective = builder.loadPerspective(Symbolizable.class, component);
+
+ assertThat(perspective).isNull();
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/task/ListTaskTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/task/ListTaskTest.java
new file mode 100644
index 00000000000..0bf7ef0afc2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/task/ListTaskTest.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.task;
+
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.task.Task;
+import org.sonar.api.task.TaskDefinition;
+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 ListTaskTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Test
+ public void should_list_available_tasks() {
+ Tasks tasks = mock(Tasks.class);
+ when(tasks.definitions()).thenReturn(Arrays.asList(
+ TaskDefinition.builder().key("foo").description("Foo").taskClass(FooTask.class).build(),
+ TaskDefinition.builder().key("purge").description("Purge database").taskClass(FakePurgeTask.class).build()));
+
+ ListTask task = new ListTask(tasks);
+
+ task.execute();
+
+ assertThat(logTester.logs(LoggerLevel.INFO)).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.INFO).get(0)).contains("Available tasks:", " - foo: Foo", " - purge: Purge database");
+ }
+
+ private static class FakePurgeTask implements Task {
+ public void execute() {
+ }
+ }
+
+ private static class FooTask implements Task {
+ public void execute() {
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/task/TasksTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/task/TasksTest.java
new file mode 100644
index 00000000000..b2d52b253f2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/task/TasksTest.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.task;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.task.Task;
+import org.sonar.api.task.TaskDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TasksTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void should_get_definitions() {
+ Tasks tasks = new Tasks(new TaskDefinition[] {ScanTask.DEFINITION, ListTask.DEFINITION});
+ assertThat(tasks.definitions()).hasSize(2);
+ }
+
+ @Test
+ public void should_get_definition_by_key() {
+ Tasks tasks = new Tasks(new TaskDefinition[] {ScanTask.DEFINITION, ListTask.DEFINITION});
+ tasks.start();
+ assertThat(tasks.definition(ListTask.DEFINITION.key())).isEqualTo(ListTask.DEFINITION);
+ }
+
+ @Test
+ public void should_return_null_if_task_not_found() {
+ Tasks tasks = new Tasks(new TaskDefinition[] {ScanTask.DEFINITION, ListTask.DEFINITION});
+
+ assertThat(tasks.definition("not-exists")).isNull();
+ }
+
+ @Test
+ public void should_fail_on_duplicated_keys() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Task 'foo' is declared twice");
+
+ new Tasks(new TaskDefinition[] {
+ TaskDefinition.builder().key("foo").taskClass(FakeTask1.class).description("foo1").build(),
+ TaskDefinition.builder().key("foo").taskClass(FakeTask2.class).description("foo2").build()
+ });
+ }
+
+ @Test
+ public void should_fail_on_duplicated_class() {
+ Tasks tasks = new Tasks(new TaskDefinition[] {
+ TaskDefinition.builder().key("foo1").taskClass(FakeTask1.class).description("foo1").build(),
+ TaskDefinition.builder().key("foo2").taskClass(FakeTask1.class).description("foo1").build()
+ });
+
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Task 'org.sonar.batch.task.TasksTest$FakeTask1' is defined twice: first by 'foo1' and then by 'foo2'");
+
+ tasks.start();
+ }
+
+ private static class FakeTask1 implements Task {
+ public void execute() {
+ }
+ }
+
+ private static class FakeTask2 implements Task {
+ public void execute() {
+ }
+
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/util/BatchUtilsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/util/BatchUtilsTest.java
new file mode 100644
index 00000000000..8de6036b5e3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/util/BatchUtilsTest.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.util;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BatchUtilsTest {
+
+ @Test
+ public void encodeForUrl() throws Exception {
+ assertThat(BatchUtils.encodeForUrl(null)).isEqualTo("");
+ assertThat(BatchUtils.encodeForUrl("")).isEqualTo("");
+ assertThat(BatchUtils.encodeForUrl("foo")).isEqualTo("foo");
+ assertThat(BatchUtils.encodeForUrl("foo&bar")).isEqualTo("foo%26bar");
+ }
+
+ @Test
+
+ public void testDescribe() {
+ Object withToString = new Object() {
+ @Override
+ public String toString() {
+ return "desc";
+ }
+ };
+
+ Object withoutToString = new Object();
+
+ assertThat(BatchUtils.describe(withToString)).isEqualTo(("desc"));
+ assertThat(BatchUtils.describe(withoutToString)).isEqualTo("java.lang.Object");
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/batch/util/ProgressReportTest.java b/sonar-scanner-engine/src/test/java/org/sonar/batch/util/ProgressReportTest.java
new file mode 100644
index 00000000000..b21ad736fde
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/batch/util/ProgressReportTest.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.util;
+
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.log.LogTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProgressReportTest {
+ private static final String THREAD_NAME = "progress";
+ private ProgressReport progressReport;
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Before
+ public void setUp() {
+ progressReport = new ProgressReport(THREAD_NAME, 100);
+ }
+
+ @Test
+ public void die_on_stop() {
+ progressReport.start("start");
+ assertThat(isThreadAlive(THREAD_NAME)).isTrue();
+ progressReport.stop("stop");
+ assertThat(isThreadAlive(THREAD_NAME)).isFalse();
+ }
+
+ @Test
+ public void do_not_block_app() {
+ progressReport.start("start");
+ assertThat(isDaemon(THREAD_NAME)).isTrue();
+ progressReport.stop("stop");
+ }
+
+ @Test
+ public void do_log() {
+ progressReport.start("start");
+ progressReport.message("Some message");
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ progressReport.stop("stop");
+ assertThat(logTester.logs()).contains("Some message");
+ }
+
+ private static boolean isDaemon(String name) {
+ Thread t = getThread(name);
+ return (t != null) && t.isDaemon();
+ }
+
+ private static boolean isThreadAlive(String name) {
+ Thread t = getThread(name);
+ return (t != null) && t.isAlive();
+ }
+
+ private static Thread getThread(String name) {
+ Set<Thread> threads = Thread.getAllStackTraces().keySet();
+
+ for (Thread t : threads) {
+ if (t.getName().equals(name)) {
+ return t;
+ }
+ }
+ return null;
+ }
+}
diff --git a/sonar-scanner-engine/src/test/resources/logback-test.xml b/sonar-scanner-engine/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..da6be3344a2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/logback-test.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+
+ <!--
+ ONLY FOR UNIT TESTS
+ -->
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} %-5level - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <!-- BeanUtils generate to many DEBUG logs when sonar.verbose is set -->
+ <logger name="org.apache.commons.beanutils.converters">
+ <level value="WARN"/>
+ </logger>
+
+ <!-- sonar.showSql -->
+ <!-- see also org.sonar.db.MyBatis#configureLogback() -->
+ <logger name="org.mybatis">
+ <level value="WARN"/>
+ </logger>
+ <logger name="org.apache.ibatis">
+ <level value="WARN"/>
+ </logger>
+ <logger name="java.sql">
+ <level value="WARN"/>
+ </logger>
+ <logger name="java.sql.ResultSet">
+ <level value="WARN"/>
+ </logger>
+ <logger name="PERSISTIT">
+ <level value="WARN"/>
+ </logger>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration>
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo
new file mode 100644
index 00000000000..74d29a4fa08
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo
@@ -0,0 +1,16 @@
+package com.sonar.it.samples.modules.a1;
+
+public class HelloA1 {
+ private int i;
+ private HelloA1() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " xoo");
+ }
+
+ protected String getHello() {
+ return "hello";
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo
new file mode 100644
index 00000000000..42039538a92
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo
@@ -0,0 +1,12 @@
+package com.sonar.it.samples.modules.a2;
+
+public class HelloA2 {
+ private int i;
+ private HelloA2() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " xoo");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo
new file mode 100644
index 00000000000..b83c3af128c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo
@@ -0,0 +1,12 @@
+package com.sonar.it.samples.modules.b1;
+
+public class HelloB1 {
+ private int i;
+ private HelloB1() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " world");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo
new file mode 100644
index 00000000000..20b8bb3876a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo
@@ -0,0 +1,12 @@
+package com.sonar.it.samples.modules.b2;
+
+public class HelloB2 {
+ private int i;
+ private HelloB2() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " world");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/sonar-project.properties b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/sonar-project.properties
new file mode 100644
index 00000000000..c2b00ede37c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample-not-associated/sonar-project.properties
@@ -0,0 +1,31 @@
+# Root project information
+#sonar.projectKey=com.sonarsource.it.samples:multi-modules-sample
+sonar.projectName=Sonar :: Integration Tests :: Multi-modules Sample
+sonar.projectVersion=1.0-SNAPSHOT
+
+sonar.language=xoo
+
+# Some properties that will be inherited by the modules
+sonar.sources=src/main/xoo
+
+# List of the module identifiers
+sonar.modules=module_a,module_b
+
+module_a.sonar.projectKey=module_a
+module_a.sonar.projectName=Module A
+
+module_a.sonar.modules=module_a1,module_a2
+
+module_a.module_a1.sonar.projectName=Sub-module A1
+
+module_a.module_a2.sonar.projectName=Sub-module A2
+
+
+module_b.sonar.projectKey=module_b
+module_b.sonar.projectName=Module B
+
+module_b.sonar.modules=module_b1,module_b2
+
+module_b.module_b1.sonar.projectName=Sub-module B1
+
+module_b.module_b2.sonar.projectName=Sub-module B2
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/.sonar/sonar-report.json b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/.sonar/sonar-report.json
new file mode 100644
index 00000000000..581142ee53c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/.sonar/sonar-report.json
@@ -0,0 +1 @@
+{"version":"5.1-SNAPSHOT","issues":[{"key":"0ae8428f-42a6-4b44-befc-512f1846fdad","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":1,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"215429f4-fa2e-4611-a75b-abb4330ea36b","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":4,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"2b5d88b5-acc5-4761-bf47-3d6d46435c59","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":10,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"50fb32b3-61af-4efa-b234-5a5b048d72a1","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":16,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"52911422-9c22-4a40-982f-c7fa779e3b2c","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":2,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"5b0da657-2183-42ae-b0f9-595a0f59de17","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":15,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"70bac047-8559-4c72-bc2d-7584df7e4a0b","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":12,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"8843a921-b9bd-4837-ae0b-423d01b2476d","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":14,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"b227df4a-78e1-4624-9f48-589000d26fe9","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":8,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"b6f8a23f-de5a-4ec2-a549-ace73f0e0667","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":9,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"c7aef709-5210-4ff2-a237-be915ad611bb","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":13,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"c86663e2-6b98-4e9f-80c3-3cdea013f816","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":11,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"cddb488f-37ba-42a6-9f2a-09090c2f1c8e","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":3,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"e1d513d2-b165-41a5-9874-b0dc01fef619","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":6,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"e49d6d6e-3e2d-425b-b531-fe761757b773","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":5,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"},{"key":"f2ba9cdb-7efe-413f-981c-e6421861db46","component":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","line":7,"message":"This issue is generated on each line","severity":"MAJOR","rule":"xoo:OneIssuePerLine","status":"OPEN","isNew":false,"creationDate":"2013-05-01T00:00:00+0200"}],"components":[{"key":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1"},{"key":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","path":"src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo","moduleKey":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1","status":"SAME"},{"key":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1","path":"src/main/xoo/com/sonar/it/samples/modules/a1","moduleKey":"com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1"}],"rules":[{"key":"xoo:OneIssuePerLine","rule":"OneIssuePerLine","repository":"xoo","name":"One Issue Per Line"}],"users":[]} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo
new file mode 100644
index 00000000000..74d29a4fa08
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo
@@ -0,0 +1,16 @@
+package com.sonar.it.samples.modules.a1;
+
+public class HelloA1 {
+ private int i;
+ private HelloA1() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " xoo");
+ }
+
+ protected String getHello() {
+ return "hello";
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo
new file mode 100644
index 00000000000..42039538a92
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo
@@ -0,0 +1,12 @@
+package com.sonar.it.samples.modules.a2;
+
+public class HelloA2 {
+ private int i;
+ private HelloA2() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " xoo");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo
new file mode 100644
index 00000000000..b83c3af128c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo
@@ -0,0 +1,12 @@
+package com.sonar.it.samples.modules.b1;
+
+public class HelloB1 {
+ private int i;
+ private HelloB1() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " world");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo
new file mode 100644
index 00000000000..20b8bb3876a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo
@@ -0,0 +1,12 @@
+package com.sonar.it.samples.modules.b2;
+
+public class HelloB2 {
+ private int i;
+ private HelloB2() {
+
+ }
+
+ public void hello() {
+ System.out.println("hello" + " world");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/sonar-project.properties b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/sonar-project.properties
new file mode 100644
index 00000000000..b07be6f3e6f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/multi-modules-sample/sonar-project.properties
@@ -0,0 +1,31 @@
+# Root project information
+sonar.projectKey=com.sonarsource.it.samples:multi-modules-sample
+sonar.projectName=Sonar :: Integration Tests :: Multi-modules Sample
+sonar.projectVersion=1.0-SNAPSHOT
+
+sonar.language=xoo
+
+# Some properties that will be inherited by the modules
+sonar.sources=src/main/xoo
+
+# List of the module identifiers
+sonar.modules=module_a,module_b
+
+module_a.sonar.projectKey=module_a
+module_a.sonar.projectName=Module A
+
+module_a.sonar.modules=module_a1,module_a2
+
+module_a.module_a1.sonar.projectName=Sub-module A1
+
+module_a.module_a2.sonar.projectName=Sub-module A2
+
+
+module_b.sonar.projectKey=module_b
+module_b.sonar.projectName=Module B
+
+module_b.sonar.modules=module_b1,module_b2
+
+module_b.module_b1.sonar.projectName=Sub-module B1
+
+module_b.module_b2.sonar.projectName=Sub-module B2
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/sonar-project.properties b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/sonar-project.properties
new file mode 100644
index 00000000000..0c8e5dc5354
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/sonar-project.properties
@@ -0,0 +1,4 @@
+sonar.projectKey=sample-multiline
+sonar.projectName=Sample Multiline
+sonar.projectVersion=0.1-SNAPSHOT
+sonar.sources=xources
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiline.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiline.xoo
new file mode 100644
index 00000000000..6e8a35f20a5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiline.xoo
@@ -0,0 +1,9 @@
+package hello;
+
+public class HelloJava {
+
+ public static void main(String[] args) {
+ {xoo-start-issue:1}System.out
+ .println("Hello"){xoo-end-issue:1};
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiple.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiple.xoo
new file mode 100644
index 00000000000..b6b1b8369a4
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Multiple.xoo
@@ -0,0 +1,9 @@
+package hello;
+
+public class HelloJava {
+
+ public static void main(String[] args) {
+ {xoo-start-issue:1}System.out.println("Hello"){xoo-end-issue:1};
+ {xoo-start-flow:1:1:1}System.out.println("World"){xoo-end-flow:1:1:1};
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Single.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Single.xoo
new file mode 100644
index 00000000000..fc664425a99
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/Single.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class HelloJava {
+
+ public static void main(String[] args) {
+ {xoo-start-issue:1}System.out.println("Hello"){xoo-end-issue:1};
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/WithFlow.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/WithFlow.xoo
new file mode 100644
index 00000000000..9dc4685fe84
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-multiline/xources/hello/WithFlow.xoo
@@ -0,0 +1,14 @@
+package hello;
+
+public class HelloJava {
+
+ public static void main(String[] args) {
+ {xoo-start-flow:1:1:1}if (true){xoo-end-flow:1:1:1} {
+ {xoo-start-flow:1:1:2}if (true){xoo-end-flow:1:1:2} {
+ {xoo-start-issue:1}if (true){xoo-end-issue:1} {
+ System.out.println("Hello");
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/sonar-project.properties b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/sonar-project.properties
new file mode 100644
index 00000000000..58f27e81f61
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/sonar-project.properties
@@ -0,0 +1,5 @@
+sonar.projectKey=sample-with-empty-file
+sonar.projectName=Sample With Empty
+sonar.projectVersion=0.1-SNAPSHOT
+sonar.sources=xources
+sonar.language=xoo
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/Empty.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/Empty.xoo
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/Empty.xoo
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/HelloJava.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/HelloJava.xoo
new file mode 100644
index 00000000000..1d9c60d56b7
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-empty-file/xources/hello/HelloJava.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class HelloJava {
+
+ public static void main(String[] args) {
+ System.out.println("Hello");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/.gitignore b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/.gitignore
new file mode 100644
index 00000000000..ecbefd4f19d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/.gitignore
@@ -0,0 +1 @@
+.sonar
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/sonar-project.properties b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/sonar-project.properties
new file mode 100644
index 00000000000..8810e376701
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/sonar-project.properties
@@ -0,0 +1,6 @@
+sonar.projectKey=sample
+sonar.projectName=Sample
+sonar.projectVersion=0.1-SNAPSHOT
+sonar.sources=xources
+sonar.tests=testx
+sonar.language=xoo
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/testx b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/testx
new file mode 120000
index 00000000000..7385ebd51cf
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/testx
@@ -0,0 +1 @@
+../sample/testx/ \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/xources b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/xources
new file mode 120000
index 00000000000..15dca9d90d2
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample-with-symlink/xources
@@ -0,0 +1 @@
+../sample/xources/ \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/sonar-project.properties b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/sonar-project.properties
new file mode 100644
index 00000000000..8810e376701
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/sonar-project.properties
@@ -0,0 +1,6 @@
+sonar.projectKey=sample
+sonar.projectName=Sample
+sonar.projectVersion=0.1-SNAPSHOT
+sonar.sources=xources
+sonar.tests=testx
+sonar.language=xoo
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo
new file mode 100644
index 00000000000..8c0967e496f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo
@@ -0,0 +1,11 @@
+package org.sonar.tests;
+
+import org.junit.Test;
+
+public class ClassOneTest {
+
+ @Test
+ public void nothing() {
+
+ }
+}
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.measures b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.measures
new file mode 100644
index 00000000000..23b08dc0e0e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.measures
@@ -0,0 +1,7 @@
+lines:11
+ncloc:7
+tests:1
+test_execution_time:1
+skipped_tests:0
+test_errors:0
+test_failures:0
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.scm b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.scm
new file mode 100644
index 00000000000..2cec35b8a72
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/testx/ClassOneTest.xoo.scm
@@ -0,0 +1,11 @@
+1,user1,2013-01-04
+1,user1,2013-01-04
+1,user1,2013-01-04
+1,user1,2013-01-04
+2,user2,2013-01-05
+2,user2,2013-01-05
+3,user3,2013-01-06
+4,user4,2013-01-07
+4,user4,2013-01-07
+4,user4,2013-01-07
+4,user4,2013-01-07 \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo
new file mode 100644
index 00000000000..1d9c60d56b7
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo
@@ -0,0 +1,8 @@
+package hello;
+
+public class HelloJava {
+
+ public static void main(String[] args) {
+ System.out.println("Hello");
+ }
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures
new file mode 100644
index 00000000000..9eaf8ba2549
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures
@@ -0,0 +1,2 @@
+ncloc:3
+complexity:1
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.scm b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.scm
new file mode 100644
index 00000000000..03a9de2f486
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.scm
@@ -0,0 +1,8 @@
+1,user1,2013-01-04
+1,user1,2013-01-04
+1,user1,2013-01-04
+1,user1,2013-01-04
+2,user2,2013-01-05
+2,user2,2013-01-05
+3,user3,2013-01-06
+4,user4,2013-01-07 \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo
new file mode 100644
index 00000000000..53cb085156c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo
@@ -0,0 +1,6 @@
+ object HelloWorld {
+ def main(args: Array[String]) {
+ println("Hello, world of xoo!")
+ }
+ }
+ \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures
new file mode 100644
index 00000000000..d2c8386aed1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures
@@ -0,0 +1,2 @@
+ncloc:5
+complexity:2
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/bootstrap/BatchPluginJarExploderTest/sonar-checkstyle-plugin-2.8.jar b/sonar-scanner-engine/src/test/resources/org/sonar/batch/bootstrap/BatchPluginJarExploderTest/sonar-checkstyle-plugin-2.8.jar
new file mode 100644
index 00000000000..f937399bec5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/bootstrap/BatchPluginJarExploderTest/sonar-checkstyle-plugin-2.8.jar
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/api_rules_list.protobuf b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/api_rules_list.protobuf
new file mode 100644
index 00000000000..1d417ce2880
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/api_rules_list.protobuf
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_issues.protobuf b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_issues.protobuf
new file mode 100644
index 00000000000..8b610d8f73c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_issues.protobuf
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_project.json b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_project.json
new file mode 100644
index 00000000000..2887ce18d10
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_project.json
@@ -0,0 +1,164 @@
+{
+ "timestamp": 0,
+ "qprofilesByLanguage": {
+ "java": {
+ "key": "java-sonar-way-72608",
+ "name": "Sonar way",
+ "language": "java",
+ "rulesUpdatedAt": "2015-08-10T12:06:53+0200"
+ }
+ },
+ "activeRules": [
+ {
+ "repositoryKey": "common-java",
+ "ruleKey": "DuplicatedBlocks",
+ "name": "Source files should not have any duplicated blocks",
+ "severity": "MAJOR",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "common-java",
+ "ruleKey": "InsufficientBranchCoverage",
+ "name": "Branches should have sufficient coverage by unit tests",
+ "severity": "MAJOR",
+ "language": "java",
+ "params": {
+ "minimumBranchCoverageRatio": "65.0"
+ }
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "RightCurlyBraceStartLineCheck",
+ "name": "A close curly brace should be located at the beginning of a line",
+ "severity": "MINOR",
+ "internalKey": "RightCurlyBraceStartLineCheck",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "UselessParenthesesCheck",
+ "name": "Useless parentheses around expressions should be removed to prevent any misunderstanding",
+ "severity": "MAJOR",
+ "internalKey": "UselessParenthesesCheck",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "ObjectFinalizeCheck",
+ "name": "The Object.finalize() method should not be called",
+ "severity": "CRITICAL",
+ "internalKey": "ObjectFinalizeCheck",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "ObjectFinalizeOverridenCheck",
+ "name": "The Object.finalize() method should not be overriden",
+ "severity": "CRITICAL",
+ "internalKey": "ObjectFinalizeOverridenCheck",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "ObjectFinalizeOverridenCallsSuperFinalizeCheck",
+ "name": "super.finalize() should be called at the end of Object.finalize() implementations",
+ "severity": "BLOCKER",
+ "internalKey": "ObjectFinalizeOverridenCallsSuperFinalizeCheck",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "ClassVariableVisibilityCheck",
+ "name": "Class variable fields should not have public accessibility",
+ "severity": "MAJOR",
+ "internalKey": "ClassVariableVisibilityCheck",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "S2188",
+ "name": "JUnit test cases should call super methods",
+ "severity": "CRITICAL",
+ "internalKey": "S2188",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "S2186",
+ "name": "JUnit assertions should not be used in \"run\" methods",
+ "severity": "CRITICAL",
+ "internalKey": "S2186",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "S2187",
+ "name": "TestCases should contain tests",
+ "severity": "MAJOR",
+ "internalKey": "S2187",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "S2391",
+ "name": "JUnit framework methods should be declared properly",
+ "severity": "CRITICAL",
+ "internalKey": "S2391",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "S2325",
+ "name": "\"private\" methods that don\u0027t access instance data should be \"static\"",
+ "severity": "MINOR",
+ "internalKey": "S2325",
+ "language": "java",
+ "params": {}
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "S1166",
+ "name": "Exception handlers should preserve the original exception",
+ "severity": "CRITICAL",
+ "internalKey": "S1166",
+ "language": "java",
+ "params": {
+ "exceptions": "java.lang.InterruptedException, java.lang.NumberFormatException, java.text.ParseException, java.net.MalformedURLException"
+ }
+ },
+ {
+ "repositoryKey": "squid",
+ "ruleKey": "S2970",
+ "name": "Assertions should be complete",
+ "severity": "CRITICAL",
+ "internalKey": "S2970",
+ "language": "java",
+ "params": {}
+ }
+
+ ],
+ "settingsByModule": {},
+ "fileDataByModuleAndPath": {
+ "org.codehaus.sonar-plugins:sonar-scm-git-plugin": {
+ "src/test/java/org/sonar/plugins/scm/git/JGitBlameCommandTest.java": {
+ "needBlame": true
+ },
+ "src/main/java/org/sonar/plugins/scm/git/GitScmProvider.java": {
+ "hash": "90082117d0dc0f1189ab7e4990a20667",
+ "needBlame": true
+ }
+ }
+ },
+ "lastAnalysisDate": "2015-08-10T13:20:09+0200"
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_users.protobuf b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_users.protobuf
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cache/ProjectCacheSynchronizerTest/batch_users.protobuf
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/cpd/ManyStatements.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cpd/ManyStatements.java
new file mode 100644
index 00000000000..ed2297068e4
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/cpd/ManyStatements.java
@@ -0,0 +1,11 @@
+package org.foo;
+
+public class ManyStatements {
+
+ void foo() {
+ int A1 = 0; int B = 0; int C = 0; int D = 0; int E = 0; int F = 0; int G = 0; int H = 0; int I = 0; int J = 0; int K = 0;
+ int A2 = 0; int B = 0; int C = 0; int D = 0; int E = 0; int F = 0; int G = 0; int H = 0; int I = 0; int J = 0; int K = 0;
+ int A1 = 0; int B = 0; int C = 0; int D = 0; int E = 0; int F = 0; int G = 0; int H = 0; int I = 0; int J = 0; int K = 0;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-mess.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-mess.txt
new file mode 100644
index 00000000000..48d30c92f97
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-mess.txt
@@ -0,0 +1,37 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ // SONAR-OFF
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+ // FOO-OFF
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ // SONAR-ON
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ // FOO-ON
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-twice.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-twice.txt
new file mode 100644
index 00000000000..9ae63dc57f9
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-twice.txt
@@ -0,0 +1,37 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ // SONAR-OFF
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+ // SONAR-ON
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ // FOO-OFF
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ // FOO-ON
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-unfinished.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-unfinished.txt
new file mode 100644
index 00000000000..dd7656180ab
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-unfinished.txt
@@ -0,0 +1,34 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ // SONAR-OFF
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-wrong-order.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-wrong-order.txt
new file mode 100644
index 00000000000..7cac0b98aed
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp-wrong-order.txt
@@ -0,0 +1,35 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ // SONAR-ON
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+ // SONAR-OFF
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp.txt
new file mode 100644
index 00000000000..002169fe031
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-double-regexp.txt
@@ -0,0 +1,35 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ // SONAR-OFF
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+ // SONAR-ON
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-no-regexp.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-no-regexp.txt
new file mode 100644
index 00000000000..f18fa5b90ad
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-no-regexp.txt
@@ -0,0 +1,33 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp-and-double-regexp.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp-and-double-regexp.txt
new file mode 100644
index 00000000000..e09ecd7a323
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp-and-double-regexp.txt
@@ -0,0 +1,36 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+ // SONAR-OFF
+
+import java.util.Set;
+
+/**
+ * @SONAR-IGNORE-ALL
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+ // SONAR-ON
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp.txt
new file mode 100644
index 00000000000..ef135ebc50c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/issue/ignore/scanner/IssueExclusionsRegexpScannerTest/file-with-single-regexp.txt
@@ -0,0 +1,33 @@
+package org.sonar.plugins.switchoffviolations.pattern;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * @SONAR-IGNORE-ALL
+ */
+public class LineRange {
+ int from, to;
+
+ public LineRange(int from, int to) {
+ if (to < from) {
+ throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to);
+ }
+ this.from = from;
+ this.to = to;
+ }
+
+ public boolean in(int lineId) {
+ return from <= lineId && lineId <= to;
+ }
+
+ public Set<Integer> toLines() {
+ Set<Integer> lines = Sets.newLinkedHashSet();
+ for (int index = from; index <= to; index++) {
+ lines.add(index);
+ }
+ return lines;
+ }
+
+} \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest/project.protobuf b/sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest/project.protobuf
new file mode 100644
index 00000000000..ce579fdbd5e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultProjectRepositoriesLoaderTest/project.protobuf
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultQualityProfileLoaderTest/quality_profile_search_default b/sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultQualityProfileLoaderTest/quality_profile_search_default
new file mode 100644
index 00000000000..6780d7338a1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/repository/DefaultQualityProfileLoaderTest/quality_profile_search_default
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search1.protobuf b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search1.protobuf
new file mode 100644
index 00000000000..5544968df4b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search1.protobuf
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search2.protobuf b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search2.protobuf
new file mode 100644
index 00000000000..a23bd1d5d81
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultActiveRulesLoaderTest/active_rule_search2.protobuf
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultRulesLoader/response.protobuf b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultRulesLoader/response.protobuf
new file mode 100644
index 00000000000..3c24dd83d29
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/DefaultRulesLoader/response.protobuf
@@ -0,0 +1,637 @@
+
+i
+ common-javaInsufficientCommentDensity">Source files should have a sufficient density of comment lines
+S
+ common-javaDuplicatedBlocks"2Source files should not have any duplicated blocks
+U
+ common-javaSkippedUnitTests"4Skipped unit tests should be either removed or fixed
+\
+ common-javaInsufficientLineCoverage"3Lines should have sufficient coverage by unit tests
+A
+ common-javaFailedUnitTests"!Failed unit tests should be fixed
+a
+ common-javaInsufficientBranchCoverage"6Branches should have sufficient coverage by unit tests
+“
+squidUselessParenthesesCheckUselessParenthesesCheck"XUseless parentheses around expressions should be removed to prevent any misunderstanding
+Z
+squidS2134S2134"CClasses extending java.lang.Thread should override the "run" method
+;
+squidS138S138"&Methods should not have too many lines
+G
+squidS2133S2133"0Objects should not be created only to "getClass"
+M
+squidS1294S1294"6The Array.equals(Object obj) method should not be used
+R
+squidS2131S2131";Primitives should not be boxed just for "String" conversion
+`
+squidS135S135"KLoops should not contain more than a single "break" or "continue" statement
+<
+squidS1150S1150"%Enumeration should not be implemented
+K
+squidS1151S1151"4"switch case" clauses should not have too many lines
+M
+squidS1939S1939"6Extensions and implementations should not be redundant
+E
+squidS2039S2039".Member variable visibility should be specified
+L
+squidNoSonarNoSonar"1"NOSONAR" should not be used to switch off issues
+>
+squidS2232S2232"'"ResultSet.isLast()" should not be used
+f
+squidS1943S1943"OClasses and methods that rely on the default system encoding should not be used
+m
+squidS1158S1158"VPrimitive wrappers should not be instantiated only for "toString" or "compareTo" calls
+H
+squidS2230S2230"1Non-public methods should not be "@Transactional"
+t
+squidS1157S1157"]Case insensitive string comparisons should be made without intermediate upper or lower casing
+P
+squidS1155S1155"9Collection.isEmpty() should be used to test for emptiness
+s
+squidS2236S2236"\Methods "wait(...)", "notify()" and "notifyAll()" should never be called on Thread instances
+b
+squidS1948S1948"KFields in a "Serializable" class should either be transient or serializable
+J
+squidS1153S1153"3String.valueOf() should not be appended to a String
+H
+squidS2235S2235"1IllegalMonitorStateException should not be caught
+b
+squidS1764S1764"KIdentical expressions should not be used on both sides of a binary operator
+P
+squidS2130S2130"9Parsing should be used to convert "Strings" to primitives
+s
+squidUndocumentedApiUndocumentedApi"HPublic types, methods and fields (API) should be documented with Javadoc
+=
+squidS2333S2333"&Redundant modifiers should not be used
+o
+squidTrailingCommentCheckTrailingCommentCheck":Comments should not be located at the end of lines of code
+m
+squidMaximumInheritanceDepthMaximumInheritanceDepth"2Inheritance tree of classes should not be too deep
+<
+squidS1940S1940"%Boolean checks should not be inverted
+L
+squidS1699S1699"5Constructors should only call non-overridable methods
+T
+squidS128S128"?Switch cases should end with an unconditional "break" statement
+M
+squidS2127S2127"6"Double.longBitsToDouble" should not be used for "int"
+X
+squidCallToDeprecatedMethodCallToDeprecatedMethod"Avoid use of deprecated methods
+]
+squidS888S888"HRelational operators should be used in "for" loop termination conditions
+A
+squidS2123S2123"*Values should not be uselessly incremented
+S
+squidS2122S2122"<"ScheduledThreadPoolExecutor" should not have 0 core threads
+P
+squidS1160S1160"9Public methods should throw at most one checked exception
+
+squidS1161S1161"x"@Override" annotation should be used on any method overriding (since Java 5) or implementing (since Java 6) another one
+W
+squidS1694S1694"@An abstract class should have both abstract and concrete methods
+=
+squidS1162S1162"&Checked Exception should not be thrown
+K
+squidS00101S00101"2Class names should comply with a naming convention
+M
+squidS1695S1695"6"NullPointerException" should not be explicitly thrown
+L
+squidS00100S00100"3Method names should comply with a naming convention
+B
+squidS1696S1696"+"NullPointerException" should not be caught
+n
+squidS1697S1697"WShort-circuit logic should be used to prevent null pointer dereferences in conditionals
+A
+squidS1698S1698"*Objects should be compared with "equals()"
+V
+squidS1168S1168"?Empty arrays and collections should be returned instead of null
+r
+squidStringEqualityComparisonCheckStringEqualityComparisonCheck"+Strings should be compared using "equals()"
+/
+squidS2222S2222"Locks should be released
+[
+squidHiddenFieldCheckHiddenFieldCheck".Local variables should not shadow class fields
+?
+squidS2326S2326"(Unused type parameters should be removed
+H
+squidS1163S1163"1Exceptions should not be thrown in finally blocks
+P
+squidS2225S2225"9"toString()" and "clone()" methods should not return null
+P
+squidS1166S1166"9Exception handlers should preserve the original exception
+<
+squidS1165S1165"%Exception classes should be immutable
+I
+squidS2226S2226"2Servlets should never have mutable instance fields
+S
+squidArchitecturalConstraintArchitecturalConstraint"Architectural constraint
+u
+squidS134S134"`Control flow statements "if", "for", "while", "switch" and "try" should not be nested too deeply
+[
+squidS2325S2325"D"private" methods that don't access instance data should be "static"
+J
+squidS2156S2156"3"final" classes should not have "protected" members
+z
+squidS2154S2154"cDissimilar primitive wrappers should not be used with the ternary operator without explicit casting
+M
+squidS2153S2153"6Boxing and unboxing should not be immediately reversed
+?
+squidS2159S2159"(Silly equality checks should not be made
+<
+squidS2157S2157"%"Cloneables" should implement "clone"
+A
+squidS1172S1172"*Unused method parameters should be removed
+R
+squidS1479S1479";"switch" statements should not have too many "case" clauses
+
+squidS1170S1170"jPublic constants and fields initialized at declaration should be "static final" rather than merely "final"
+D
+squidS1171S1171"-Only static class initializers should be used
+]
+squidS1175S1175"FThe signature of "finalize()" should match that of "Object.finalize()"
+b
+squidS1174S1174"K"Object.finalize()" should remain protected (versus public) when overriding
+A
+squidS2151S2151"*"runFinalizersOnExit" should not be called
+f
+squidCallToFileDeleteOnExitMethodCallToFileDeleteOnExitMethod"!"deleteOnExit" should not be used
+Z
+squidLabelsShouldNotBeUsedCheckLabelsShouldNotBeUsedCheck"Labels should not be used
+e
+squidS1488S1488"NLocal Variables should not be declared and then immediately returned or thrown
+Z
+squidS3008S3008"CStatic non-final field names should comply with a naming convention
+{
+squidSwitchLastCaseIsDefaultCheckSwitchLastCaseIsDefaultCheck"6"switch" statements should end with a "default" clause
+Ë
+squid,RightCurlyBraceDifferentLineAsNextBlockCheck,RightCurlyBraceDifferentLineAsNextBlockCheck"fClose curly brace and the next "else", "catch" and "finally" keywords should be on two different lines
+d
+squidModifiersOrderCheckModifiersOrderCheck"1Modifiers should be declared in the correct order
+?
+squidS1181S1181"(Throwable and Error should not be caught
+9
+squidS1905S1905""Redundant casts should not be used
+c
+squidS1182S1182"LClasses that override "clone" should be "Cloneable" and call "super.clone()"
+j
+squidS2201S2201"SReturn values should not be ignored when function calls don't have any side effects
+2
+squidS1186S1186"Methods should not be empty
+=
+squidS1872S1872"&Classes should not be compared by name
+R
+squidS2438S2438";"Threads" should not be used where "Runnables" are expected
+u
+squidS1871S1871"^Two branches in the same conditional structure should not have exactly the same implementation
+l
+squidS1185S1185"UOverriding methods should do more than simply call the same method in the super class
+S
+squidS1188S1188"<Lambdas and anonymous classes should not have too many lines
+@
+squidS1873S1873")"static final" arrays should be "private"
+\
+squidS2204S2204"E".equals()" should not be used to test the values of "Atomic" classes
+C
+squidS2437S2437",Silly bit operations should not be performed
+T
+squidS2200S2200"="compareTo" results should not be checked for specific values
+R
+squidUselessImportCheckUselessImportCheck"!Useless imports should be removed
+E
+squidS2209S2209"."static" members should be accessed statically
+?
+squidS1481S1481"(Unused local variables should be removed
+€
+squidMissingDeprecatedCheckMissingDeprecatedCheck"GDeprecated elements should have both the annotation and the Javadoc tag
+:
+squidS2208S2208"#Wildcard imports should not be used
+>
+squidS1774S1774"'The ternary operator should not be used
+V
+squidS2272S2272"?"Iterator.next()" methods should throw "NoSuchElementException"
+‰
+squidS2273S2273"r"wait(...)", "notify()" and "notifyAll()" methods should only be called when a lock is obviously held on an object
+a
+squidLowerCaseLongSuffixCheckLowerCaseLongSuffixCheck"$Long suffix "L" should be upper case
+C
+squidS2786S2786",Nested "enum"s should not be declared static
+J
+squidS1118S1118"3Utility classes should not have public constructors
+z
+squidS2277S2277"cCryptographic RSA algorithms should always incorporate OAEP (Optimal Asymmetric Encryption Padding)
+a
+squidUnusedProtectedMethodUnusedProtectedMethod"*Unused protected methods should be removed
+d
+squidS2276S2276"M"wait(...)" should be used instead of "Thread.sleep(...)" when a lock is held
+d
+squidS2275S2275"MPrintf-style format strings should not lead to unexpected behavior at runtime
+k
+squidS2274S2274"T"Object.wait(...)" and "Condition.await(...)" should be called inside a "while" loop
+c
+squidCommentedOutCodeLineCommentedOutCodeLine".Sections of code should not be "commented out"
+^
+squidS2278S2278"GNeither DES (Data Encryption Standard) nor DESede (3DES) should be used
+C
+squidS2912S2912","indexOf" checks should use a start position
+<
+squidS1656S1656"%Variables should not be self-assigned
+Q
+squidS1659S1659":Multiple variables should not be declared on the same line
+L
+squidS1264S1264"5A "while" loop should be used instead of a "for" loop
+Y
+squidS1125S1125"BLiteral boolean values should not be used in condition expressions
+k
+squidS1126S1126"TReturn of boolean expressions should not be wrapped into an "if-then-else" statement
+
+squidClassVariableVisibilityCheckClassVariableVisibilityCheck":Class variable fields should not have public accessibility
+H
+squidS2250S2250"1"ConcurrentLinkedQueue.size()" should not be used
+M
+squidS1643S1643"6Strings should not be concatenated using '+' in a loop
+`
+squidS2251S2251"IA "for" loop update clause should move the counter in the right direction
+Z
+squidS1640S1640"CMaps with keys that are enum values should be replaced with EnumMap
+E
+squidS2118S2118".Non-serializable classes should not be written
+B
+squidS00112S00112")Generic exceptions should never be thrown
+>
+squidS2111S2111"'"BigDecimal(double)" should not be used
+H
+squidS2112S2112"1"URL.hashCode" and "URL.equals" should be avoided
+?
+squidS2110S2110"(Invalid "Date" values should not be used
+X
+squidS2116S2116"A"hashCode" and "toString" should not be called on array instances
+J
+squidS2391S2391"3JUnit framework methods should be declared properly
+Y
+squidS2114S2114"BCollections should not be passed as arguments to their own methods
+?
+squidS2259S2259"(Null pointers should not be dereferenced
+d
+squidS1132S1132"MStrings literals should be placed on the left side when checking for equality
+D
+squidS00107S00107"+Methods should not have too many parameters
+c
+squidS2258S2258"L"javax.crypto.NullCipher" should not be used for anything other than testing
+C
+squidS1133S1133",Deprecated code should be removed eventually
+L
+squidS2257S2257"5Only standard cryptographic algorithms should be used
+G
+squidS00108S00108".Nested blocks of code should not be left empty
+5
+squidS00103S00103"Lines should not be too long
+V
+squidS2254S2254"?"HttpServletRequest.getRequestedSessionId()" should not be used
+<
+squidS2253S2253"%Disallowed methods should not be used
+5
+squidS1134S1134""FIXME" tags should be handled
+A
+squidS00105S00105"(Tabulation characters should not be used
+C
+squidS2252S2252",Loop conditions should be true at least once
+4
+squidS1135S1135""TODO" tags should be handled
+=
+squidS00104S00104"$Files should not have too many lines
+?
+squidS00122S00122"&Statements should be on separate lines
+M
+squidS00120S00120"4Package names should comply with a naming convention
+U
+squidS2109S2109">Reflection should not be used to check non-runtime annotations
+J
+squidS00121S00121"1Control structures should always use curly braces
+j
+squidS2676S2676"SNeither "Math.abs" nor negation should be used on numbers that could be "MIN_VALUE"
+I
+squidS2677S2677"2"read" and "readLine" return values should be used
+N
+squidS2674S2674"7The value returned from a stream read should be checked
+@
+squidS2675S2675")"readObject" should not be "synchronized"
+V
+squidCycleBetweenPackagesCycleBetweenPackages"!Avoid cycle between java packages
+h
+squidS1149S1149"QSynchronized classes Vector, Hashtable, Stack and StringBuffer should not be used
+O
+squidS1244S1244"8Floating point numbers should not be tested for equality
+P
+squidS2384S2384"9Mutable members should not be stored or returned directly
+{
+squidLeftCurlyBraceEndLineCheckLeftCurlyBraceEndLineCheck":An open curly brace should be located at the end of a line
+Q
+squidS2387S2387":Child class members should not shadow parent class members
+P
+squidS2386S2386"9Interfaces should not have "public static" mutable fields
+U
+squidS2388S2388">Inner class calls to super class methods should be unambiguous
+<
+squidS1141S1141"%Try-catch blocks should not be nested
+L
+squidS1142S1142"5Methods should not contain too many return statements
+T
+squidS00119S00119";Type parameter names should comply with a naming convention
+c
+squidS2245S2245"LPseudorandom number generators (PRNGs) should not be used in secure contexts
+O
+squidS1143S1143"8"return" statements should not occur in "finally" blocks
+T
+squidS00118S00118";Abstract class names should comply with a naming convention
+i
+squidS00117S00117"PLocal variable and method parameter names should comply with a naming convention
+]
+squidS1145S1145"FUseless "if(true) {...}" and "if(false){...}" blocks should be removed
+K
+squidS00116S00116"2Field names should comply with a naming convention
+N
+squidS00115S00115"5Constant names should comply with a naming convention
+…
+squidLeftCurlyBraceStartLineCheckLeftCurlyBraceStartLineCheck"@An open curly brace should be located at the beginning of a line
+8
+squidS1147S1147"!Exit methods should not be called
+O
+squidS00114S00114"6Interface names should comply with a naming convention
+J
+squidS1148S1148"3Throwable.printStackTrace(...) should not be called
+J
+squidS00113S00113"1Files should contain an empty new line at the end
+]
+squidS1215S1215"FExecution of the Garbage Collector should be triggered only by the JVM
+T
+squidS1217S1217"=Thread.run() and Runnable.run() should not be called directly
+A
+squidS2188S2188"*JUnit test cases should call super methods
+M
+squidS1219S1219"6"switch" statements should not contain non-case labels
+K
+squidS2186S2186"4JUnit assertions should not be used in "run" methods
+5
+squidS2187S2187"TestCases should contain tests
+i
+squidS1210S1210"R"equals(Object obj)" should be overridden along with the "compareTo(T obj)" method
+D
+squidS1214S1214"-Constants should not be defined in interfaces
+o
+squidS1609S1609"X@FunctionalInterface annotation should be used to flag Single Abstract Method interfaces
+l
+squidS1213S1213"UThe members of an interface declaration or class should appear in a pre-defined order
+d
+squidObjectFinalizeCheckObjectFinalizeCheck"1The Object.finalize() method should not be called
+<
+squidS2089S2089"%HTTP referers should not be relied on
+s
+squidS1611S1611"\Parentheses should be removed from a single lambda input parameter when its type is inferred
+J
+squidS2681S2681"3Multiline blocks should be enclosed in curly braces
+K
+squidS1612S1612"4Replace lambdas with method references when possible
+9
+squidS2185S2185""Silly math should not be performed
+E
+squidS2184S2184".Math operands should be cast before assignment
+X
+squidS1610S1610"AAbstract classes without fields should be converted to interfaces
+_
+squidS2183S2183"HInts and longs should not be shifted by more than their number of bits-1
+8
+squid EmptyFile EmptyFile"Files should not be empty
+N
+squidS1228S1228"7Packages should have a javadoc file 'package-info.java'
+Z
+squidUnusedPrivateMethodUnusedPrivateMethod"'Unused private method should be removed
+j
+squidS1226S1226"SMethod parameters, caught exceptions and foreach variables should not be reassigned
+P
+squidS2197S2197"9Modulus results should not be checked for direct equality
+ƒ
+squidAssignmentInSubExpressionCheckAssignmentInSubExpressionCheck":Assignments should not be made from within sub-expressions
+H
+squidS1221S1221"1Methods should not be named "hashcode" or "equal"
+E
+squidS1220S1220".The default unnamed package should not be used
+1
+squidS2092S2092"Cookies should be "secure"
+2
+squidS2094S2094"Classes should not be empty
+1
+squidS2095S2095"Resources should be closed
+9
+squidS2096S2096"""main" should not "throw" anything
+c
+squidS1223S1223"LNon-constructor methods should not have the same name as the enclosing class
+E
+squidS2097S2097"."equals(Object obj)" should test argument type
+K
+squidS2696S2696"4Instance methods should not write to "static" fields
+6
+squidS2699S2699"Tests should include assertions
+?
+squidS2698S2698"(JUnit assertions should include messages
+D
+squidS2693S2693"-Threads should not be started in constructors
+J
+squidS2692S2692"3"indexOf" checks should not be for positive numbers
+f
+squidS2695S2695"O"PreparedStatement" and "ResultSet" methods should be called with valid indices
+c
+squidS2694S2694"LInner classes which do not reference their owning classes should be "static"
+I
+squidS2885S2885"2"Calendars" and "DateFormats" should not be static
+]
+squidS2166S2166"FClasses named like "Exception" should extend "Exception" or a subclass
+H
+squidS2167S2167"1"compareTo" should not return "Integer.MIN_VALUE"
+=
+squidS2164S2164"&Math should not be performed on floats
+A
+squidS2165S2165"*"finalize" should not set fields to "null"
+s
+squidS1994S1994"\"for" loop incrementers should modify the variable being tested in the loop's stop condition
+x
+squidRedundantThrowsDeclarationCheckRedundantThrowsDeclarationCheck"-Throws declarations should not be superfluous
+S
+squidS2162S2162"<"equals" methods should be symmetric and work for subclasses
+J
+squidS2160S2160"3Subclasses that add fields should override "equals"
+J
+squidS2175S2175"3Inappropriate "Collection" calls should not be made
+o
+squidForLoopCounterChangedCheckForLoopCounterChangedCheck"."for" loop stop conditions should be invariant
+O
+squidS2176S2176"8Class names should not shadow interfaces or superclasses
+M
+squidS2178S2178"6Short-circuit logic should be used in boolean contexts
+T
+squidS1700S1700"=A field should not duplicate the name of its containing class
+Z
+squidS1206S1206"C"equals(Object obj)" and "hashCode()" should be overridden in pairs
+K
+squidS1701S1701"4Fields and methods should not have conflicting names
+S
+squidS1201S1201"<Methods named "equals" should override Object.equals(Object)
+p
+squidS1200S1200"YClasses should not be coupled to too many other classes (Single Responsibility Principle)
+`
+squidClassCyclomaticComplexityClassCyclomaticComplexity"!Classes should not be too complex
+f
+squidS1602S1602"OLamdbas containing only one statement should not nest this statement in a block
+_
+squidS1604S1604"HAnonymous inner classes containing only one method should become lambdas
+J
+squidS1309S1309"3The @SuppressWarnings annotation should not be used
+K
+squidS1607S1607"4Skipped unit tests should be either removed or fixed
+P
+squidS1301S1301"9"switch" statements should have at least 3 "case" clauses
+1
+squidS2446S2446""notifyAll" should be used
+r
+squidS2445S2445"[Blocks synchronized on fields should not contain assignments of new objects to those fields
+V
+squidS2444S2444"?Lazy initialization of "static" fields should be "synchronized"
+M
+squidS1858S1858"6"toString()" should never be called on a String object
+y
+squidObjectFinalizeOverridenCheckObjectFinalizeOverridenCheck"4The Object.finalize() method should not be overriden
+B
+squidS2442S2442"+"Lock" objects should not be "synchronized"
+V
+squidS2441S2441"?Non-serializable objects should not be stored in "HttpSessions"
+T
+squidS2440S2440"=Classes with only "static" methods should not be instantiated
+C
+squidS1710S1710",Annotation repetitions should not be wrapped
+]
+squidS2583S2583"FConditions should not unconditionally evaluate to "TRUE" or to "FALSE"
+d
+squidS1850S1850"M"instanceof" operators that always return "true" or "false" should be removed
+J
+squidS2447S2447"3Null should not be returned from a "Boolean" method
+F
+squidS1310S1310"/"NOPMD" suppression comments should not be used
+d
+squidS864S864"OLimited dependence should be placed on operator precedence rules in expressions
+;
+squidS1313S1313"$IP addresses should not be hardcoded
+d
+squidS1312S1312"MLoggers should be "private static final" and should share a naming convention
+š
+squidS1319S1319"‚Declarations should use Java collection interfaces such as "List" rather than specific implementation classes such as "LinkedList"
+W
+squidS1318S1318"@"object == null" should be used instead of "object.equals(null)"
+U
+squidS1452S1452">Generic wildcard types should not be used in return parameters
+F
+squidS1451S1451"/Copyright and license headers should be defined
+X
+squidIndentationCheckIndentationCheck"+Source code should be indented consistently
+_
+squidS2718S2718"H"DateUtils.truncate" from Apache Commons Lang library should not be used
+O
+squidS1315S1315"8"CHECKSTYLE:OFF" suppression comments should not be used
+6
+squidS1314S1314"Octal values should not be used
+e
+squidS1317S1317"N"StringBuilder" and "StringBuffer" should not be instantiated with a character
+Y
+squidS1860S1860"BSynchronization should not be based on Strings or boxed primitives
+Y
+squidS1862S1862"BRelated "if/else if" statements should not have the same condition
+[
+squidS1724S1724"DDeprecated classes and interfaces should not be extended/implemented
+z
+squidS881S881"eIncrement (++) and decrement (--) operators should not be mixed with other operators in an expression
+8
+squid ParsingError ParsingError"Java parser failure
+M
+squidS1598S1598"6Package declaration should match source file directory
+u
+squidS2055S2055"^The non-serializable super class of a "Serializable" class must have a no-argument constructor
+“
+squidS1596S1596"|Collections.emptyList(), emptyMap() and emptySet() should be used instead of Collections.EMPTY_LIST, EMPTY_MAP and EMPTY_SET
+F
+squidS2057S2057"/"Serializable" classes should have a version id
+^
+squidS2059S2059"G"Serializable" inner classes of "Serializable" classes should be static
+N
+squidS2701S2701"7Literal boolean values should not be used in assertions
+;
+squidS2063S2063"$Comparators should be "Serializable"
+Ã
+squid'RightCurlyBraceSameLineAsNextBlockCheck'RightCurlyBraceSameLineAsNextBlockCheck"hClose curly brace and the next "else", "catch" and "finally" keywords should be located on the same line
+V
+squidS2061S2061"?Custom serialization method signatures should meet requirements
+C
+squidS1066S1066",Collapsible "if" statements should be merged
+<
+squidS1067S1067"%Expressions should not be too complex
+_
+squidEmptyStatementUsageCheckEmptyStatementUsageCheck""Empty statements should be removed
+b
+squidMethodCyclomaticComplexityMethodCyclomaticComplexity"!Methods should not be too complex
+6
+squidS1065S1065"Unused labels should be removed
+>
+squidS1068S1068"'Unused private fields should be removed
+U
+squidS2976S2976">"File.createTempFile" should not be used to create a directory
+N
+squidS2974S2974"7Classes without "public" constructors should be "final"
+;
+squidS2068S2068"$Credentials should not be hard-coded
+4
+squidS2970S2970"Assertions should be complete
+S
+squidS2065S2065"<Fields in non-serializable classes should not be "transient"
+b
+squidS2066S2066"K"Serializable" inner classes of non-serializable classes should be "static"
+U
+squidS1197S1197">Array designators "[]" should be on the type, not the variable
+
+squidS1844S1844"j"Object.wait(...)" should never be called on objects that implement "java.util.concurrent.locks.Condition"
+<
+squidS1199S1199"%Nested code blocks should not be used
+a
+squidS1848S1848"JObjects should not be created to be dropped immediately without being used
+K
+squidS2301S2301"4Public methods should not contain selector arguments
+M
+squidS1849S1849"6"Iterator.hasNext()" should not call "Iterator.next()"
+R
+squidS2070S2070";SHA-1 and Message-Digest hash algorithms should not be used
+‡
+squidRightCurlyBraceStartLineCheckRightCurlyBraceStartLineCheck"@A close curly brace should be located at the beginning of a line
+]
+squidS2864S2864"F"entrySet()" should be iterated when both the key and value are needed
+
+squidS1444S1444":
+G
+squidS1191S1191"0Classes from "sun.*" packages should not be used
+B
+squidS1190S1190"+Future keywords should not be used as names
+^
+squidS1193S1193"GException types should not be tested using "instanceof" in catch blocks
+G
+squidS2076S2076"0Values passed to OS commands should be sanitized
+H
+squidS2077S2077"1Values passed to SQL commands should be sanitized
+5
+squidS109S109" Magic numbers should not be used
+?
+squidS1192S1192"(String literals should not be duplicated
+P
+squidS106S106";Standard ouputs should not be used directly to log anything
+c
+squidS1195S1195"LArray designators "[]" should be located after the type in method signatures
+H
+squidS2078S2078"1Values passed to LDAP queries should be sanitized
+º
+squid.ObjectFinalizeOverridenCallsSuperFinalizeCheck.ObjectFinalizeOverridenCallsSuperFinalizeCheck"Qsuper.finalize() should be called at the end of Object.finalize() implementations
+?
+squidS1194S1194"("java.lang.Error" should not be extended \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/ModuleQProfilesTest/shared.xml b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/ModuleQProfilesTest/shared.xml
new file mode 100644
index 00000000000..feb234f20e8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/rule/ModuleQProfilesTest/shared.xml
@@ -0,0 +1,19 @@
+<dataset>
+
+ <rules_profiles id="1" name="Java One" language="java" parent_kee="[null]" kee="java-one" is_default="[false]"
+ created_at="2014-01-20" updated_at="2014-01-20"
+ rules_updated_at="2014-01-20T12:00:00+0000"/>
+
+ <rules_profiles id="2" name="Java Two" language="java" parent_kee="[null]" kee="java-two" is_default="[false]"
+ created_at="2014-01-20" updated_at="2014-01-20"
+ rules_updated_at="2014-01-20T12:00:00+0000"/>
+
+ <rules_profiles id="3" name="Php One" language="php" parent_kee="[null]" kee="php-one" is_default="[false]"
+ created_at="2014-01-20" updated_at="2014-01-20"
+ rules_updated_at="2014-01-20T12:00:00+0000"/>
+
+ <rules_profiles id="4" name="Cobol One" language="cbl" parent_kee="[null]" kee="cobol-one" is_default="[false]"
+ created_at="2014-01-20" updated_at="2014-01-20"
+ rules_updated_at="2014-01-20T12:00:00+0000"/>
+
+</dataset>
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sonar-project.properties
new file mode 100644
index 00000000000..7bace22a204
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sonar-project.properties
@@ -0,0 +1 @@
+# Mandatory properties for module1 are all inferred from the module ID
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sources/Fake.java
new file mode 100644
index 00000000000..9d445f04fc6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module1/sources/Fake.java
@@ -0,0 +1,5 @@
+
+class Fake {
+
+
+}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/newBaseDir/src/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/newBaseDir/src/Fake.java
new file mode 100644
index 00000000000..22d579be381
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/newBaseDir/src/Fake.java
@@ -0,0 +1 @@
+class Fake { }
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/sonar-project.properties
new file mode 100644
index 00000000000..d25a9e9e1f5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/module2/sonar-project.properties
@@ -0,0 +1,6 @@
+sonar.projectKey=com.foo.project.module2
+sonar.projectName=Foo Module 2
+# redefine some properties
+sonar.projectBaseDir=newBaseDir
+sonar.projectDescription=Description of Module 2
+sonar.sources=src
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/sonar-project.properties
new file mode 100644
index 00000000000..4744284e7bf
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module-inherited/sonar-project.properties
@@ -0,0 +1,11 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1,\
+ module2
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sonar-project.properties
new file mode 100644
index 00000000000..ec642a9443a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sonar-project.properties
@@ -0,0 +1,4 @@
+sonar.projectKey=com.foo.project.module1
+sonar.projectName=Foo Module 1
+sonar.projectDescription=Description of Module 1
+sonar.sources=sources
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/newBaseDir/src/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/newBaseDir/src/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/newBaseDir/src/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/sonar-project.properties
new file mode 100644
index 00000000000..d25a9e9e1f5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/module2/sonar-project.properties
@@ -0,0 +1,6 @@
+sonar.projectKey=com.foo.project.module2
+sonar.projectName=Foo Module 2
+# redefine some properties
+sonar.projectBaseDir=newBaseDir
+sonar.projectDescription=Description of Module 2
+sonar.sources=src
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/sonar-project.properties
new file mode 100644
index 00000000000..2f16886c91d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-definitions-in-each-module/sonar-project.properties
@@ -0,0 +1,10 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1,\
+ module2
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/generated/any-file.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/generated/any-file.properties
new file mode 100644
index 00000000000..c50d50b50f7
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/generated/any-file.properties
@@ -0,0 +1,5 @@
+sonar.projectKey=com.foo.project.module1
+sonar.projectName=Foo Module 1
+
+# and specify a different baseDir
+sonar.projectBaseDir=..
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/any-folder/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/sonar-project.properties
new file mode 100644
index 00000000000..c1640b15cba
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile-and-overwritten-basedir/sonar-project.properties
@@ -0,0 +1,12 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1
+
+module1.sonar.projectConfigFile=any-folder/generated/any-file.properties
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/any-file.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/any-file.properties
new file mode 100644
index 00000000000..460d3495a89
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/any-file.properties
@@ -0,0 +1,2 @@
+sonar.projectKey=com.foo.project.module1
+sonar.projectName=Foo Module 1
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/any-folder/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/sonar-project.properties
new file mode 100644
index 00000000000..e246f8c2af8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-configfile/sonar-project.properties
@@ -0,0 +1,12 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1
+
+module1.sonar.projectConfigFile=any-folder/any-file.properties
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-unexisting-file/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-unexisting-file/sonar-project.properties
new file mode 100644
index 00000000000..e246f8c2af8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/DeprecatedProjectReactorBuilderTest/multi-module-with-unexisting-file/sonar-project.properties
@@ -0,0 +1,12 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1
+
+module1.sonar.projectConfigFile=any-folder/any-file.properties
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module11/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module11/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module11/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module12/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module12/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module1/module12/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module2/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module2/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/module2/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/sonar-project.properties
new file mode 100644
index 00000000000..e14567d26af
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/big-multi-module-definitions-all-in-root/sonar-project.properties
@@ -0,0 +1,17 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+sonar.profile=Foo
+
+sonar.modules=module1,module2
+
+
+module1.sonar.modules=module11,module12
+
+module1.module11.property=My module11 property
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/sonar-project.properties
new file mode 100644
index 00000000000..53aacb54061
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/sonar-project.properties
@@ -0,0 +1,13 @@
+sonar.projectKey=example
+sonar.projectName=Example
+sonar.projectVersion=1.0
+
+sonar.modules=java-module,groovy-module
+
+java-module.sonar.language=java
+java-module.sonar.projectBaseDir=.
+java-module.sonar.sources=src/main/java
+
+groovy-module.sonar.language=groovy
+groovy-module.sonar.projectBaseDir=.
+groovy-module.sonar.sources=src/main/groovy
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/groovy/Fake.groovy b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/groovy/Fake.groovy
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/groovy/Fake.groovy
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/java/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/java/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-language-definitions-all-in-root/src/main/java/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module2/src/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module2/src/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/module2/src/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/sonar-project.properties
new file mode 100644
index 00000000000..0f06d31466e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-all-in-root/sonar-project.properties
@@ -0,0 +1,19 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1,\
+ module2
+
+# Mandatory properties for module1 are all inferred from the module ID
+
+module2.sonar.projectKey=com.foo.project.module2
+module2.sonar.projectName=Foo Module 2
+# redefine some properties
+module2.sonar.projectDescription=Description of Module 2
+module2.sonar.sources=src
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module2/src/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module2/src/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/module2/src/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/sonar-project.properties
new file mode 100644
index 00000000000..6def8070d6f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-moduleKey/sonar-project.properties
@@ -0,0 +1,19 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1,\
+ module2
+
+# Mandatory properties for module1 are all inferred from the module ID
+
+module2.sonar.moduleKey=com.foo.project.module2
+module2.sonar.projectName=Foo Module 2
+# redefine some properties
+module2.sonar.projectDescription=Description of Module 2
+module2.sonar.sources=src
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1.feature/src/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1.feature/src/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1.feature/src/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/sonar-project.properties
new file mode 100644
index 00000000000..2a6221a3757
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-definitions-same-prefix/sonar-project.properties
@@ -0,0 +1,19 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1,\
+ module1.feature
+
+# Mandatory properties for module1 are all inferred from the module ID
+
+module1.feature.sonar.projectKey=com.foo.project.module1.feature
+module1.feature.sonar.projectName=Foo Module 1 Feature
+# redefine some properties
+module1.feature.sonar.projectDescription=Description of Module 1 Feature
+module1.feature.sonar.sources=src
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-duplicate-id/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-duplicate-id/sonar-project.properties
new file mode 100644
index 00000000000..0a971805d13
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-duplicate-id/sonar-project.properties
@@ -0,0 +1,12 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1,module1
+
+module1.sonar.sources=src
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/modules/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/sonar-project.properties
new file mode 100644
index 00000000000..e63d3da926c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-repeated-id/sonar-project.properties
@@ -0,0 +1,17 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1
+
+module1.sonar.projectBaseDir=modules/module1
+module1.sonar.projectName=Foo Module 1
+module1.sonar.modules=module1
+
+module1.module1.sonar.projectBaseDir=module1
+module1.module1.sonar.projectName=Foo Sub Module 1
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/modules/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/modules/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/modules/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/sonar-project.properties
new file mode 100644
index 00000000000..c572ef1f9e5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir-not-associated/sonar-project.properties
@@ -0,0 +1,14 @@
+#sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1
+
+module1.sonar.projectBaseDir=modules/module1
+module1.sonar.projectKey=com.foo.project.module1
+module1.sonar.projectName=Foo Module 1
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/modules/module1/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/modules/module1/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/modules/module1/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/sonar-project.properties
new file mode 100644
index 00000000000..615f5c77a7f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-basedir/sonar-project.properties
@@ -0,0 +1,14 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1
+
+module1.sonar.projectBaseDir=modules/module1
+module1.sonar.projectKey=com.foo.project.module1
+module1.sonar.projectName=Foo Module 1
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/module1/src/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/module1/src/Fake.java
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/module1/src/Fake.java
@@ -0,0 +1 @@
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/sonar-project.properties
new file mode 100644
index 00000000000..09cb2208fcd
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-explicit-unexisting-test-dir/sonar-project.properties
@@ -0,0 +1,9 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=src
+
+sonar.modules=module1
+module1.sonar.tests=tests
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-basedir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-basedir/sonar-project.properties
new file mode 100644
index 00000000000..67fbf347c0e
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-basedir/sonar-project.properties
@@ -0,0 +1,13 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
+sonar.binaries=target/classes
+
+sonar.modules=module1
+
+module1.sonar.projectKey=com.foo.project.module1
+module1.sonar.projectName=Foo Module 1
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/module1/src/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/module1/src/Fake.java
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/module1/src/Fake.java
@@ -0,0 +1 @@
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/sonar-project.properties
new file mode 100644
index 00000000000..04ea08a89a1
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/multi-module-with-unexisting-source-dir/sonar-project.properties
@@ -0,0 +1,8 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=unexisting-source-dir
+
+sonar.modules=module1
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetFile/foo.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetFile/foo.properties
new file mode 100644
index 00000000000..8fbb104c92d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetFile/foo.properties
@@ -0,0 +1,4 @@
+prop= foo, bar, \
+toto,\
+\
+tutu, \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetList/foo.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetList/foo.properties
new file mode 100644
index 00000000000..8fbb104c92d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/shouldGetList/foo.properties
@@ -0,0 +1,4 @@
+prop= foo, bar, \
+toto,\
+\
+tutu, \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-blank-source-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-blank-source-dir/sonar-project.properties
new file mode 100644
index 00000000000..ba79992d5a0
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-blank-source-dir/sonar-project.properties
@@ -0,0 +1,6 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/build/report.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/build/report.txt
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/build/report.txt
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sonar-project.properties
new file mode 100644
index 00000000000..35b33996ea7
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sonar-project.properties
@@ -0,0 +1,8 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+sonar.projectBuildDir=build
+
+sonar.sources=sources
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sources/Fake.java
new file mode 100644
index 00000000000..aee03e60b4a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-build-dir/sources/Fake.java
@@ -0,0 +1,3 @@
+package org.sonar.runner.batch.ProjectReactorBuilderTest.simple;
+
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/lib/Fake.class b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/lib/Fake.class
new file mode 100644
index 00000000000..bf2c3a09e07
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/lib/Fake.class
@@ -0,0 +1,3 @@
+package org.sonar.runner.batch.ProjectReactorBuilderTest.simple
+
+Fake \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sonar-project.properties
new file mode 100644
index 00000000000..0cada50b51f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.libraries=lib
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sources/Fake.java
new file mode 100644
index 00000000000..aee03e60b4a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-lib-dir/sources/Fake.java
@@ -0,0 +1,3 @@
+package org.sonar.runner.batch.ProjectReactorBuilderTest.simple;
+
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-missing-source-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-missing-source-dir/sonar-project.properties
new file mode 100644
index 00000000000..3a7a65335dc
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-missing-source-dir/sonar-project.properties
@@ -0,0 +1,5 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sonar-project.properties
new file mode 100644
index 00000000000..55d1ddf0242
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.binaries=bin
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-binary/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sonar-project.properties
new file mode 100644
index 00000000000..69ccd8d1411
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.libraries=libs/*.txt
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-lib/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-source-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-source-dir/sonar-project.properties
new file mode 100644
index 00000000000..0b83b11f29c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-source-dir/sonar-project.properties
@@ -0,0 +1,6 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=unexisting-source-dir
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sonar-project.properties
new file mode 100644
index 00000000000..a4fac8e4ca6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.tests=tests
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project-with-unexisting-test-dir/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib1.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib1.txt
new file mode 100644
index 00000000000..81d4e95a0b6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib1.txt
@@ -0,0 +1 @@
+lib1 \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib2.txt b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib2.txt
new file mode 100644
index 00000000000..7dacac0fd9a
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/libs/lib2.txt
@@ -0,0 +1 @@
+lib2 \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sonar-project.properties b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sonar-project.properties
new file mode 100644
index 00000000000..69ccd8d1411
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.projectKey=com.foo.project
+sonar.projectName=Foo Project
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.projectDescription=Description of Foo Project
+
+sonar.sources=sources
+sonar.libraries=libs/*.txt
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sources/Fake.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sources/Fake.java
new file mode 100644
index 00000000000..e67004defc5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/ProjectReactorBuilderTest/simple-project/sources/Fake.java
@@ -0,0 +1 @@
+class Fake {}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report-without-resolved-issues.json b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report-without-resolved-issues.json
new file mode 100644
index 00000000000..b5af45efe6c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report-without-resolved-issues.json
@@ -0,0 +1,28 @@
+{
+ "version": "3.6",
+ "issues": [],
+ "components": [
+ {"key": "struts"},
+ {
+ "key": "struts-core",
+ "path": "core"
+ },
+ {
+ "key": "struts-ui",
+ "path": "ui"
+ },
+ {
+ "key": "struts:src/main/java/org/apache/struts/Action.java",
+ "path": "src/main/java/org/apache/struts/Action.java",
+ "moduleKey": "struts",
+ "status": "CHANGED"
+ },
+ {
+ "key": "struts:src/main/java/org/apache/struts",
+ "path": "src/main/java/org/apache/struts",
+ "moduleKey": "struts"
+ }
+ ],
+ "rules": [],
+ "users": []
+}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report.json b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report.json
new file mode 100644
index 00000000000..a33e06342fa
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/scan/report/JSONReportTest/report.json
@@ -0,0 +1,64 @@
+{
+ "version": "3.6",
+ "issues": [
+ {
+ "key": "200",
+ "component": "struts:src/main/java/org/apache/struts/Action.java",
+ "line": 1,
+ "startLine": 1,
+ "startOffset": 3,
+ "endLine": 2,
+ "endOffset": 4,
+ "message": "There are 2 cycles",
+ "severity": "MINOR",
+ "rule": "squid:AvoidCycles",
+ "status": "OPEN",
+ "isNew": false,
+ "assignee": "simon",
+ "effortToFix": 3.14,
+ "creationDate": "${json-unit.ignore}"
+ }
+ ],
+ "components": [
+ {
+ "key": "struts"
+ },
+ {
+ "key": "struts-core",
+ "path": "core"
+ },
+ {
+ "key": "struts-ui",
+ "path": "ui"
+ },
+ {
+ "key": "struts:src/main/java/org/apache/struts/Action.java",
+ "path": "src/main/java/org/apache/struts/Action.java",
+ "moduleKey": "struts",
+ "status": "CHANGED"
+ },
+ {
+ "key": "struts:src/main/java/org/apache/struts",
+ "path": "src/main/java/org/apache/struts",
+ "moduleKey": "struts"
+ }
+ ],
+ "rules": [
+ {
+ "key": "squid:AvoidCycles",
+ "rule": "AvoidCycles",
+ "repository": "squid",
+ "name": "Avoid Cycles"
+ }
+ ],
+ "users": [
+ {
+ "login": "julien",
+ "name": "Julien"
+ },
+ {
+ "login": "simon",
+ "name": "Simon"
+ }
+ ]
+}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.java b/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.java
new file mode 100644
index 00000000000..c5cc9793730
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.java
@@ -0,0 +1,12 @@
+/**
+ * Doc
+ */
+public class Person {
+
+ private int first;
+
+ @Deprecated
+ public void foo(int first, String last, Double middle) {
+ this.first = first; // First
+ }
+}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.js b/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.js
new file mode 100644
index 00000000000..fc36e5aa127
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/Person.js
@@ -0,0 +1,8 @@
+/**
+ * Doc
+ */
+var Person = function(first, last, middle) {
+ this.first = first; // First
+ this.middle = '';
+ this.last = 1;
+};
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/package.html b/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/package.html
new file mode 100644
index 00000000000..f2d90e627d6
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/batch/source/CodeColorizersTest/package.html
@@ -0,0 +1 @@
+<!-- $Header: /cvshome/build/org.osgi.service.log/src/org/osgi/service/log/package.html,v 1.3 2005/08/10 01:43:20 hargrave Exp $ --> <BODY> <P>The OSGi Log Service Package. Specification Version 1.3. <p>Bundles wishing to use this package must list the package in the Import-Package header of the bundle's manifest. For example: <pre> Import-Package: org.osgi.service.log; version=1.3 </pre> </BODY> \ No newline at end of file