aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-plugin-api-impl/src/main
diff options
context:
space:
mode:
authorDuarte Meneses <duarte.meneses@sonarsource.com>2019-06-19 13:56:51 -0500
committerSonarTech <sonartech@sonarsource.com>2019-07-12 20:21:14 +0200
commit93dc9770902dc7e168869d88b5ad731bfc0bedd9 (patch)
tree97ba885661d5cd9a2115fe212df31bacec9f9947 /sonar-plugin-api-impl/src/main
parent7c7d9b6b90244d2c974207862071caccdb2c9bb5 (diff)
downloadsonarqube-93dc9770902dc7e168869d88b5ad731bfc0bedd9.tar.gz
sonarqube-93dc9770902dc7e168869d88b5ad731bfc0bedd9.zip
Extract implementation from plugin API and create new module sonar-plugin-api-impl
Diffstat (limited to 'sonar-plugin-api-impl/src/main')
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultActiveRule.java100
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRule.java110
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRuleParam.java46
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/ConfigurationBridge.java52
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MapSettings.java112
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MultivalueProperty.java208
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/package-info.java23
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/MetadataLoader.java78
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/PluginContextImpl.java89
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/SonarRuntimeImpl.java94
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/AbstractProjectOrModule.java161
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultFileSystem.java246
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultIndexedFile.java161
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputComponent.java72
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputDir.java122
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputFile.java439
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputModule.java81
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputProject.java39
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextPointer.java74
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextRange.java74
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/FileMetadata.java161
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/Metadata.java71
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/PathPattern.java136
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/SensorStrategy.java41
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/TestInputFileBuilder.java278
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/CharHandler.java35
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/FileHashComputer.java81
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/IntArrayList.java117
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineCounter.java82
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineHashComputer.java81
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineOffsetCounter.java74
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbsolutePathPredicate.java63
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbstractFilePredicate.java57
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AndPredicate.java103
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicates.java214
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FalsePredicate.java45
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicate.java62
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FilenamePredicate.java45
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/LanguagePredicate.java38
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/NotPredicate.java48
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OperatorPredicate.java32
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicate.java49
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicateAdapter.java46
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OrPredicate.java76
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/PathPatternPredicate.java42
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/RelativePathPredicate.java69
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/StatusPredicate.java42
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TruePredicate.java44
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TypePredicate.java40
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/URIPredicate.java65
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/AbstractDefaultIssue.java122
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssue.java93
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssueLocation.java92
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultNoSonarFilter.java32
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/ActiveRulesBuilder.java49
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultActiveRules.java85
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultRules.java91
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/RulesBuilder.java50
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAdHocRule.java126
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAnalysisError.java87
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCoverage.java157
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCpdTokens.java136
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultExternalIssue.java135
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultHighlighting.java126
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultMeasure.java139
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultPostJobDescriptor.java56
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSensorDescriptor.java123
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSignificantCode.java75
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultStorable.java57
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSymbolTable.java148
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/InMemorySensorStorage.java146
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SensorContextTester.java399
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SyntaxHighlightingRule.java53
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultDebtRemediationFunctions.java68
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewParam.java85
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRepository.java113
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRule.java350
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultParam.java87
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRepository.java128
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRule.java238
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/RuleDefinitionContext.java97
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2.java71
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/DefaultTempFolder.java126
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/JUnitTempFolder.java108
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/ScannerUtils.java74
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java52
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/WorkDuration.java194
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/package-info.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/PartImpl.java44
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/SimpleGetRequest.java148
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/ValidatingRequest.java242
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/package-info.java23
99 files changed, 9735 insertions, 0 deletions
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultActiveRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultActiveRule.java
new file mode 100644
index 00000000000..a46bf1a2e82
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultActiveRule.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.batch.rule;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+
+@Immutable
+public class DefaultActiveRule implements ActiveRule {
+ private final RuleKey ruleKey;
+ private final String severity;
+ private final String internalKey;
+ private final String language;
+ private final String templateRuleKey;
+ private final Map<String, String> params;
+ private final long createdAt;
+ private final long updatedAt;
+ private final String qProfileKey;
+
+ public DefaultActiveRule(NewActiveRule newActiveRule) {
+ this.severity = newActiveRule.severity;
+ this.internalKey = newActiveRule.internalKey;
+ this.templateRuleKey = newActiveRule.templateRuleKey;
+ this.ruleKey = newActiveRule.ruleKey;
+ this.params = Collections.unmodifiableMap(new HashMap<>(newActiveRule.params));
+ this.language = newActiveRule.language;
+ this.createdAt = newActiveRule.createdAt;
+ this.updatedAt = newActiveRule.updatedAt;
+ this.qProfileKey = newActiveRule.qProfileKey;
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return ruleKey;
+ }
+
+ @Override
+ public String severity() {
+ return severity;
+ }
+
+ @Override
+ public String language() {
+ return language;
+ }
+
+ @Override
+ public String param(String key) {
+ return params.get(key);
+ }
+
+ @Override
+ public Map<String, String> params() {
+ // already immutable
+ return params;
+ }
+
+ @Override
+ public String internalKey() {
+ return internalKey;
+ }
+
+ @Override
+ public String templateRuleKey() {
+ return templateRuleKey;
+ }
+
+ public long createdAt() {
+ return createdAt;
+ }
+
+ public long updatedAt() {
+ return updatedAt;
+ }
+
+ @Override
+ public String qpKey() {
+ return qProfileKey;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRule.java
new file mode 100644
index 00000000000..b8938529024
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRule.java
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.batch.rule;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleStatus;
+
+@Immutable
+public class DefaultRule implements Rule {
+
+ private final RuleKey key;
+ private final Integer id;
+ private final String name;
+ private final String severity;
+ private final String type;
+ private final String description;
+ private final String internalKey;
+ private final RuleStatus status;
+ private final Map<String, RuleParam> params;
+
+ public DefaultRule(NewRule newRule) {
+ this.key = newRule.key;
+ this.id = newRule.id;
+ this.name = newRule.name;
+ this.severity = newRule.severity;
+ this.type = newRule.type;
+ this.description = newRule.description;
+ this.internalKey = newRule.internalKey;
+ this.status = newRule.status;
+
+ Map<String, RuleParam> builder = new HashMap<>();
+ for (NewRuleParam newRuleParam : newRule.params.values()) {
+ builder.put(newRuleParam.key, new DefaultRuleParam(newRuleParam));
+ }
+ params = Collections.unmodifiableMap(builder);
+ }
+
+ @Override
+ public RuleKey key() {
+ return key;
+ }
+
+ @CheckForNull
+ public Integer id() {
+ return id;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String severity() {
+ return severity;
+ }
+
+ @CheckForNull
+ public String type() {
+ return type;
+ }
+
+ @Override
+ public String description() {
+ return description;
+ }
+
+ @Override
+ public String internalKey() {
+ return internalKey;
+ }
+
+ @Override
+ public RuleStatus status() {
+ return status;
+ }
+
+ @Override
+ public RuleParam param(String paramKey) {
+ return params.get(paramKey);
+ }
+
+ @Override
+ public Collection<RuleParam> params() {
+ return params.values();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRuleParam.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRuleParam.java
new file mode 100644
index 00000000000..eb714dcfeda
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/rule/DefaultRuleParam.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.batch.rule;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+class DefaultRuleParam implements RuleParam {
+
+ private final String key;
+ private final String description;
+
+ DefaultRuleParam(NewRuleParam p) {
+ this.key = p.key;
+ this.description = p.description;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ @Nullable
+ public String description() {
+ return description;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/ConfigurationBridge.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/ConfigurationBridge.java
new file mode 100644
index 00000000000..fea9800f3a8
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/ConfigurationBridge.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.config;
+
+import java.util.Optional;
+import org.sonar.api.config.Settings;
+import org.sonar.api.config.Configuration;
+
+/**
+ * Used to help migration from {@link Settings} to {@link Configuration}
+ */
+public class ConfigurationBridge implements Configuration {
+
+ private final Settings settings;
+
+ public ConfigurationBridge(Settings settings) {
+ this.settings = settings;
+ }
+
+ @Override
+ public Optional<String> get(String key) {
+ return Optional.ofNullable(settings.getString(key));
+ }
+
+ @Override
+ public boolean hasKey(String key) {
+ return settings.hasKey(key);
+ }
+
+ @Override
+ public String[] getStringArray(String key) {
+ return settings.getStringArray(key);
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MapSettings.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MapSettings.java
new file mode 100644
index 00000000000..61ac6013c3f
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MapSettings.java
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.config;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.Encryption;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+
+import static java.util.Collections.unmodifiableMap;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * In-memory map-based implementation of {@link Settings}. It must be used
+ * <b>only for unit tests</b>. This is not the implementation
+ * deployed at runtime, so non-test code must never cast
+ * {@link Settings} to {@link MapSettings}.
+ *
+ * @since 6.1
+ */
+public class MapSettings extends Settings {
+
+ private final Map<String, String> props = new HashMap<>();
+ private final ConfigurationBridge configurationBridge;
+
+ public MapSettings() {
+ this(new PropertyDefinitions());
+ }
+
+ public MapSettings(PropertyDefinitions definitions) {
+ super(definitions, new Encryption(null));
+ configurationBridge = new ConfigurationBridge(this);
+ }
+
+ @Override
+ protected Optional<String> get(String key) {
+ return Optional.ofNullable(props.get(key));
+ }
+
+ @Override
+ protected void set(String key, String value) {
+ props.put(
+ requireNonNull(key, "key can't be null"),
+ requireNonNull(value, "value can't be null").trim());
+ }
+
+ @Override
+ protected void remove(String key) {
+ props.remove(key);
+ }
+
+ @Override
+ public Map<String, String> getProperties() {
+ return unmodifiableMap(props);
+ }
+
+ /**
+ * Delete all properties
+ */
+ public MapSettings clear() {
+ props.clear();
+ return this;
+ }
+
+ @Override
+ public MapSettings setProperty(String key, String value) {
+ return (MapSettings) super.setProperty(key, value);
+ }
+
+ @Override
+ public MapSettings setProperty(String key, Integer value) {
+ return (MapSettings) super.setProperty(key, value);
+ }
+
+ @Override
+ public MapSettings setProperty(String key, Boolean value) {
+ return (MapSettings) super.setProperty(key, value);
+ }
+
+ @Override
+ public MapSettings setProperty(String key, Long value) {
+ return (MapSettings) super.setProperty(key, value);
+ }
+
+ /**
+ * @return a {@link Configuration} proxy on top of this existing {@link Settings} implementation. Changes are reflected in the {@link Configuration} object.
+ * @since 6.5
+ */
+ public Configuration asConfig() {
+ return configurationBridge;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MultivalueProperty.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MultivalueProperty.java
new file mode 100644
index 00000000000..3d7d9f009c7
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/MultivalueProperty.java
@@ -0,0 +1,208 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.config;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+import org.apache.commons.lang.ArrayUtils;
+
+public class MultivalueProperty {
+ private MultivalueProperty() {
+ // prevents instantiation
+ }
+
+ public static String[] parseAsCsv(String key, String value) {
+ return parseAsCsv(key, value, Function.identity());
+ }
+
+ public static String[] parseAsCsv(String key, String value, Function<String, String> valueProcessor) {
+ String cleanValue = MultivalueProperty.trimFieldsAndRemoveEmptyFields(value);
+ List<String> result = new ArrayList<>();
+ try (CSVParser csvParser = CSVFormat.RFC4180
+ .withHeader((String) null)
+ .withIgnoreEmptyLines()
+ .withIgnoreSurroundingSpaces()
+ .parse(new StringReader(cleanValue))) {
+ List<CSVRecord> records = csvParser.getRecords();
+ if (records.isEmpty()) {
+ return ArrayUtils.EMPTY_STRING_ARRAY;
+ }
+ processRecords(result, records, valueProcessor);
+ return result.toArray(new String[result.size()]);
+ } catch (IOException e) {
+ throw new IllegalStateException("Property: '" + key + "' doesn't contain a valid CSV value: '" + value + "'", e);
+ }
+ }
+
+ /**
+ * In most cases we expect a single record. <br>Having multiple records means the input value was splitted over multiple lines (this is common in Maven).
+ * For example:
+ * <pre>
+ * &lt;sonar.exclusions&gt;
+ * src/foo,
+ * src/bar,
+ * src/biz
+ * &lt;sonar.exclusions&gt;
+ * </pre>
+ * In this case records will be merged to form a single list of items. Last item of a record is appended to first item of next record.
+ * <p>
+ * This is a very curious case, but we try to preserve line break in the middle of an item:
+ * <pre>
+ * &lt;sonar.exclusions&gt;
+ * a
+ * b,
+ * c
+ * &lt;sonar.exclusions&gt;
+ * </pre>
+ * will produce ['a\nb', 'c']
+ */
+ private static void processRecords(List<String> result, List<CSVRecord> records, Function<String, String> valueProcessor) {
+ for (CSVRecord csvRecord : records) {
+ Iterator<String> it = csvRecord.iterator();
+ if (!result.isEmpty()) {
+ String next = it.next();
+ if (!next.isEmpty()) {
+ int lastItemIdx = result.size() - 1;
+ String previous = result.get(lastItemIdx);
+ if (previous.isEmpty()) {
+ result.set(lastItemIdx, valueProcessor.apply(next));
+ } else {
+ result.set(lastItemIdx, valueProcessor.apply(previous + "\n" + next));
+ }
+ }
+ }
+ it.forEachRemaining(s -> {
+ String apply = valueProcessor.apply(s);
+ result.add(apply);
+ });
+ }
+ }
+
+ /**
+ * Removes the empty fields from the value of a multi-value property from empty fields, including trimming each field.
+ * <p>
+ * Quotes can be used to prevent an empty field to be removed (as it is used to preserve empty spaces).
+ * <ul>
+ * <li>{@code "" => ""}</li>
+ * <li>{@code " " => ""}</li>
+ * <li>{@code "," => ""}</li>
+ * <li>{@code ",," => ""}</li>
+ * <li>{@code ",,," => ""}</li>
+ * <li>{@code ",a" => "a"}</li>
+ * <li>{@code "a," => "a"}</li>
+ * <li>{@code ",a," => "a"}</li>
+ * <li>{@code "a,,b" => "a,b"}</li>
+ * <li>{@code "a, ,b" => "a,b"}</li>
+ * <li>{@code "a,\"\",b" => "a,b"}</li>
+ * <li>{@code "\"a\",\"b\"" => "\"a\",\"b\""}</li>
+ * <li>{@code "\" a \",\"b \"" => "\" a \",\"b \""}</li>
+ * <li>{@code "\"a\",\"\",\"b\"" => "\"a\",\"\",\"b\""}</li>
+ * <li>{@code "\"a\",\" \",\"b\"" => "\"a\",\" \",\"b\""}</li>
+ * <li>{@code "\" a,,b,c \",\"d \"" => "\" a,,b,c \",\"d \""}</li>
+ * <li>{@code "a,\" \",b" => "ab"]}</li>
+ * </ul>
+ */
+ static String trimFieldsAndRemoveEmptyFields(String str) {
+ char[] chars = str.toCharArray();
+ char[] res = new char[chars.length];
+ /*
+ * set when reading the first non trimmable char after a separator char (or the beginning of the string)
+ * unset when reading a separator
+ */
+ boolean inField = false;
+ boolean inQuotes = false;
+ int i = 0;
+ int resI = 0;
+ for (; i < chars.length; i++) {
+ boolean isSeparator = chars[i] == ',';
+ if (!inQuotes && isSeparator) {
+ // exiting field (may already be unset)
+ inField = false;
+ if (resI > 0) {
+ resI = retroTrim(res, resI);
+ }
+ } else {
+ boolean isTrimmed = !inQuotes && istrimmable(chars[i]);
+ if (isTrimmed && !inField) {
+ // we haven't meet any non trimmable char since the last separator yet
+ continue;
+ }
+
+ boolean isEscape = isEscapeChar(chars[i]);
+ if (isEscape) {
+ inQuotes = !inQuotes;
+ }
+
+ // add separator as we already had one field
+ if (!inField && resI > 0) {
+ res[resI] = ',';
+ resI++;
+ }
+
+ // register in field (may already be set)
+ inField = true;
+ // copy current char
+ res[resI] = chars[i];
+ resI++;
+ }
+ }
+ // inQuotes can only be true at this point if quotes are unbalanced
+ if (!inQuotes) {
+ // trim end of str
+ resI = retroTrim(res, resI);
+ }
+ return new String(res, 0, resI);
+ }
+
+ private static boolean isEscapeChar(char aChar) {
+ return aChar == '"';
+ }
+
+ private static boolean istrimmable(char aChar) {
+ return aChar <= ' ';
+ }
+
+ /**
+ * Reads from index {@code resI} to the beginning into {@code res} looking up the location of the trimmable char with
+ * the lowest index before encountering a non-trimmable char.
+ * <p>
+ * This basically trims {@code res} from any trimmable char at its end.
+ *
+ * @return index of next location to put new char in res
+ */
+ private static int retroTrim(char[] res, int resI) {
+ int i = resI;
+ while (i >= 1) {
+ if (!istrimmable(res[i - 1])) {
+ return i;
+ }
+ i--;
+ }
+ return i;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/package-info.java
new file mode 100644
index 00000000000..bcccfddddd4
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/config/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.config;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/MetadataLoader.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/MetadataLoader.java
new file mode 100644
index 00000000000..575fc472592
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/MetadataLoader.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.context;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+import org.sonar.api.SonarEdition;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
+
+import static org.apache.commons.lang.StringUtils.trimToEmpty;
+
+/**
+ * For internal use
+ *
+ * @since 7.8
+ */
+public class MetadataLoader {
+
+ private static final String VERSION_FILE_PATH = "/sonar-api-version.txt";
+ private static final String EDITION_FILE_PATH = "/sonar-edition.txt";
+
+ private MetadataLoader() {
+ // only static methods
+ }
+
+ public static Version loadVersion(System2 system) {
+ URL url = system.getResource(VERSION_FILE_PATH);
+
+ try (Scanner scanner = new Scanner(url.openStream(), StandardCharsets.UTF_8.name())) {
+ String versionInFile = scanner.nextLine();
+ return Version.parse(versionInFile);
+ } catch (IOException e) {
+ throw new IllegalStateException("Can not load " + VERSION_FILE_PATH + " from classpath ", e);
+ }
+ }
+
+ public static SonarEdition loadEdition(System2 system) {
+ URL url = system.getResource(EDITION_FILE_PATH);
+ if (url == null) {
+ return SonarEdition.COMMUNITY;
+ }
+ try (Scanner scanner = new Scanner(url.openStream(), StandardCharsets.UTF_8.name())) {
+ String editionInFile = scanner.nextLine();
+ return parseEdition(editionInFile);
+ } catch (IOException e) {
+ throw new IllegalStateException("Can not load " + EDITION_FILE_PATH + " from classpath", e);
+ }
+ }
+
+ static SonarEdition parseEdition(String edition) {
+ String str = trimToEmpty(edition.toUpperCase());
+ try {
+ return SonarEdition.valueOf(str);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException(String.format("Invalid edition found in '%s': '%s'", EDITION_FILE_PATH, str));
+ }
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/PluginContextImpl.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/PluginContextImpl.java
new file mode 100644
index 00000000000..211e5cedbc2
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/PluginContextImpl.java
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.context;
+
+import org.sonar.api.Plugin;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.impl.config.MapSettings;
+
+/**
+ * Implementation of {@link Plugin.Context} that plugins could use in their unit tests.
+ *
+ * Example:
+ *
+ * <pre>
+ * import org.sonar.api.internal.SonarRuntimeImpl;
+ * import org.sonar.api.config.internal.MapSettings;
+ *
+ * ...
+ *
+ * SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 1), SonarQubeSide.SCANNER);
+ * MapSettings settings = new MapSettings().setProperty("foo", "bar");
+ * Plugin.Context context = new PluginContextImpl.Builder()
+ * .setSonarRuntime(runtime)
+ * .setBootConfiguration(settings.asConfig());
+ * .build();
+ * </pre>
+ *
+ * @since 7.1
+ */
+public class PluginContextImpl extends Plugin.Context {
+
+ private final Configuration bootConfiguration;
+
+ private PluginContextImpl(Builder builder) {
+ super(builder.sonarRuntime);
+ this.bootConfiguration = builder.bootConfiguration != null ? builder.bootConfiguration : new MapSettings().asConfig();
+ }
+
+ @Override
+ public Configuration getBootConfiguration() {
+ return bootConfiguration;
+ }
+
+ public static class Builder {
+ private SonarRuntime sonarRuntime;
+ private Configuration bootConfiguration;
+
+ /**
+ * Required.
+ * @see SonarRuntimeImpl
+ * @return this
+ */
+ public Builder setSonarRuntime(SonarRuntime r) {
+ this.sonarRuntime = r;
+ return this;
+ }
+
+ /**
+ * If not set, then an empty configuration is used.
+ * @return this
+ */
+ public Builder setBootConfiguration(Configuration c) {
+ this.bootConfiguration = c;
+ return this;
+ }
+
+ public Plugin.Context build() {
+ return new PluginContextImpl(this);
+ }
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/SonarRuntimeImpl.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/SonarRuntimeImpl.java
new file mode 100644
index 00000000000..4e4074efdbb
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/SonarRuntimeImpl.java
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.context;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.SonarEdition;
+import org.sonar.api.SonarProduct;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.utils.Version;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+/**
+ * @since 6.0
+ */
+@Immutable
+public class SonarRuntimeImpl implements SonarRuntime {
+
+ private final Version version;
+ private final SonarProduct product;
+ private final SonarQubeSide sonarQubeSide;
+ private final SonarEdition edition;
+
+ private SonarRuntimeImpl(Version version, SonarProduct product, @Nullable SonarQubeSide sonarQubeSide, @Nullable SonarEdition edition) {
+ this.edition = edition;
+ requireNonNull(product);
+ checkArgument((product == SonarProduct.SONARQUBE) == (sonarQubeSide != null), "sonarQubeSide should be provided only for SonarQube product");
+ checkArgument((product == SonarProduct.SONARQUBE) == (edition != null), "edition should be provided only for SonarQube product");
+ this.version = requireNonNull(version);
+ this.product = product;
+ this.sonarQubeSide = sonarQubeSide;
+ }
+
+ @Override
+ public Version getApiVersion() {
+ return version;
+ }
+
+ @Override
+ public SonarProduct getProduct() {
+ return product;
+ }
+
+ @Override
+ public SonarQubeSide getSonarQubeSide() {
+ if (sonarQubeSide == null) {
+ throw new UnsupportedOperationException("Can only be called in SonarQube");
+ }
+ return sonarQubeSide;
+ }
+
+ @Override
+ public SonarEdition getEdition() {
+ if (sonarQubeSide == null) {
+ throw new UnsupportedOperationException("Can only be called in SonarQube");
+ }
+ return edition;
+ }
+
+ /**
+ * Create an instance for SonarQube runtime environment.
+ */
+ public static SonarRuntime forSonarQube(Version version, SonarQubeSide side, SonarEdition edition) {
+ return new SonarRuntimeImpl(version, SonarProduct.SONARQUBE, side, edition);
+ }
+
+ /**
+ * Create an instance for SonarLint runtime environment.
+ */
+ public static SonarRuntime forSonarLint(Version version) {
+ return new SonarRuntimeImpl(version, SonarProduct.SONARLINT, null, null);
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/package-info.java
new file mode 100644
index 00000000000..632005ca779
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/context/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.context;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/AbstractProjectOrModule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/AbstractProjectOrModule.java
new file mode 100644
index 00000000000..2834b345a93
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/AbstractProjectOrModule.java
@@ -0,0 +1,161 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.concurrent.Immutable;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.SystemUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+@Immutable
+public abstract class AbstractProjectOrModule extends DefaultInputComponent {
+ private static final Logger LOGGER = Loggers.get(AbstractProjectOrModule.class);
+ private final Path baseDir;
+ private final Path workDir;
+ private final String name;
+ private final String originalName;
+ private final String description;
+ private final String keyWithBranch;
+ private final String branch;
+ private final Map<String, String> properties;
+
+ private final String key;
+ private final ProjectDefinition definition;
+ private final Charset encoding;
+
+ public AbstractProjectOrModule(ProjectDefinition definition, int scannerComponentId) {
+ super(scannerComponentId);
+ this.baseDir = initBaseDir(definition);
+ this.workDir = initWorkingDir(definition);
+ this.name = definition.getName();
+ this.originalName = definition.getOriginalName();
+ this.description = definition.getDescription();
+ this.keyWithBranch = definition.getKeyWithBranch();
+ this.branch = definition.getBranch();
+ this.properties = Collections.unmodifiableMap(new HashMap<>(definition.properties()));
+
+ this.definition = definition;
+ this.key = definition.getKey();
+ this.encoding = initEncoding(definition);
+ }
+
+ private static Charset initEncoding(ProjectDefinition module) {
+ String encodingStr = module.properties().get(CoreProperties.ENCODING_PROPERTY);
+ Charset result;
+ if (StringUtils.isNotEmpty(encodingStr)) {
+ result = Charset.forName(StringUtils.trim(encodingStr));
+ } else {
+ result = Charset.defaultCharset();
+ }
+ return result;
+ }
+
+ private static Path initBaseDir(ProjectDefinition module) {
+ Path result;
+ try {
+ result = module.getBaseDir().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to resolve module baseDir", e);
+ }
+ return result;
+ }
+
+ private static Path initWorkingDir(ProjectDefinition module) {
+ File workingDirAsFile = module.getWorkDir();
+ Path workingDir = workingDirAsFile.getAbsoluteFile().toPath().normalize();
+ if (SystemUtils.IS_OS_WINDOWS) {
+ try {
+ Files.createDirectories(workingDir);
+ Files.setAttribute(workingDir, "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
+ } catch (IOException e) {
+ LOGGER.warn("Failed to set working directory hidden: {}", e.getMessage());
+ }
+ }
+ return workingDir;
+ }
+
+ /**
+ * Module key without branch
+ */
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public boolean isFile() {
+ return false;
+ }
+
+ public ProjectDefinition definition() {
+ return definition;
+ }
+
+ public Path getBaseDir() {
+ return baseDir;
+ }
+
+ public Path getWorkDir() {
+ return workDir;
+ }
+
+ public String getKeyWithBranch() {
+ return keyWithBranch;
+ }
+
+ @CheckForNull
+ public String getBranch() {
+ return branch;
+ }
+
+ public Map<String, String> properties() {
+ return properties;
+ }
+
+ @CheckForNull
+ public String getOriginalName() {
+ return originalName;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Charset getEncoding() {
+ return encoding;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultFileSystem.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultFileSystem.java
new file mode 100644
index 00000000000..db015d9277d
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultFileSystem.java
@@ -0,0 +1,246 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.StreamSupport;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FilePredicates;
+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.impl.fs.predicates.DefaultFilePredicates;
+import org.sonar.api.impl.fs.predicates.FileExtensionPredicate;
+import org.sonar.api.impl.fs.predicates.OptimizedFilePredicateAdapter;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.PathUtils;
+
+/**
+ * @since 4.2
+ */
+public class DefaultFileSystem implements FileSystem {
+
+ private final Cache cache;
+ private final FilePredicates predicates;
+ private final Path baseDir;
+ private Path workDir;
+ private Charset encoding;
+
+ /**
+ * Only for testing
+ */
+ public DefaultFileSystem(Path baseDir) {
+ this(baseDir, new MapCache(), new DefaultFilePredicates(baseDir));
+ }
+
+ /**
+ * Only for testing
+ */
+ public DefaultFileSystem(File baseDir) {
+ this(baseDir.toPath(), new MapCache(), new DefaultFilePredicates(baseDir.toPath()));
+ }
+
+ protected DefaultFileSystem(Path baseDir, Cache cache, FilePredicates filePredicates) {
+ this.baseDir = baseDir;
+ this.cache = cache;
+ this.predicates = filePredicates;
+ }
+
+ public Path baseDirPath() {
+ return baseDir;
+ }
+
+ @Override
+ public File baseDir() {
+ return baseDir.toFile();
+ }
+
+ public DefaultFileSystem setEncoding(Charset e) {
+ this.encoding = e;
+ return this;
+ }
+
+ @Override
+ public Charset encoding() {
+ return encoding;
+ }
+
+ public DefaultFileSystem setWorkDir(Path d) {
+ this.workDir = d;
+ return this;
+ }
+
+ @Override
+ public File workDir() {
+ return workDir.toFile();
+ }
+
+ @Override
+ public InputFile inputFile(FilePredicate predicate) {
+ Iterable<InputFile> files = inputFiles(predicate);
+ Iterator<InputFile> iterator = files.iterator();
+ if (!iterator.hasNext()) {
+ return null;
+ }
+ InputFile first = iterator.next();
+ if (!iterator.hasNext()) {
+ return first;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("expected one element but was: <" + first);
+ for (int i = 0; i < 4 && iterator.hasNext(); i++) {
+ sb.append(", " + iterator.next());
+ }
+ if (iterator.hasNext()) {
+ sb.append(", ...");
+ }
+ sb.append('>');
+
+ throw new IllegalArgumentException(sb.toString());
+
+ }
+
+ public Iterable<InputFile> inputFiles() {
+ return inputFiles(predicates.all());
+ }
+
+ @Override
+ public Iterable<InputFile> inputFiles(FilePredicate predicate) {
+ return OptimizedFilePredicateAdapter.create(predicate).get(cache);
+ }
+
+ @Override
+ public boolean hasFiles(FilePredicate predicate) {
+ return inputFiles(predicate).iterator().hasNext();
+ }
+
+ @Override
+ public Iterable<File> files(FilePredicate predicate) {
+ return () -> StreamSupport.stream(inputFiles(predicate).spliterator(), false)
+ .map(InputFile::file)
+ .iterator();
+ }
+
+ @Override
+ public InputDir inputDir(File dir) {
+ String relativePath = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), dir));
+ if (relativePath == null) {
+ return null;
+ }
+ // Issues on InputDir are moved to the project, so we just return a fake InputDir for backward compatibility
+ return new DefaultInputDir("unused", relativePath).setModuleBaseDir(baseDir);
+ }
+
+ public DefaultFileSystem add(InputFile inputFile) {
+ cache.add(inputFile);
+ return this;
+ }
+
+ @Override
+ public SortedSet<String> languages() {
+ return cache.languages();
+ }
+
+ @Override
+ public FilePredicates predicates() {
+ return predicates;
+ }
+
+ public abstract static class Cache implements Index {
+
+ protected abstract void doAdd(InputFile inputFile);
+
+ final void add(InputFile inputFile) {
+ doAdd(inputFile);
+ }
+
+ protected abstract SortedSet<String> languages();
+ }
+
+ /**
+ * Used only for testing
+ */
+ private static class MapCache extends Cache {
+ private final Map<String, InputFile> fileMap = new HashMap<>();
+ private final Map<String, Set<InputFile>> filesByNameCache = new HashMap<>();
+ private final Map<String, Set<InputFile>> filesByExtensionCache = new HashMap<>();
+ private SortedSet<String> languages = new TreeSet<>();
+
+ @Override
+ public Iterable<InputFile> inputFiles() {
+ return new ArrayList<>(fileMap.values());
+ }
+
+ @Override
+ public InputFile inputFile(String relativePath) {
+ return fileMap.get(relativePath);
+ }
+
+ @Override
+ public Iterable<InputFile> getFilesByName(String filename) {
+ return filesByNameCache.get(filename);
+ }
+
+ @Override
+ public Iterable<InputFile> getFilesByExtension(String extension) {
+ return filesByExtensionCache.get(extension);
+ }
+
+ @Override
+ protected void doAdd(InputFile inputFile) {
+ if (inputFile.language() != null) {
+ languages.add(inputFile.language());
+ }
+ fileMap.put(inputFile.relativePath(), inputFile);
+ filesByNameCache.computeIfAbsent(inputFile.filename(), x -> new HashSet<>()).add(inputFile);
+ filesByExtensionCache.computeIfAbsent(FileExtensionPredicate.getExtension(inputFile), x -> new HashSet<>()).add(inputFile);
+ }
+
+ @Override
+ protected SortedSet<String> languages() {
+ return languages;
+ }
+ }
+
+ @Override
+ public File resolvePath(String path) {
+ File file = new File(path);
+ if (!file.isAbsolute()) {
+ try {
+ file = new File(baseDir(), path).getCanonicalFile();
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Unable to resolve path '" + path + "'", e);
+ }
+ }
+ return file;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultIndexedFile.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultIndexedFile.java
new file mode 100644
index 00000000000..5dae95717c4
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultIndexedFile.java
@@ -0,0 +1,161 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.batch.fs.IndexedFile;
+import org.sonar.api.batch.fs.InputFile.Type;
+import org.sonar.api.utils.PathUtils;
+
+/**
+ * @since 6.3
+ */
+@Immutable
+public class DefaultIndexedFile extends DefaultInputComponent implements IndexedFile {
+ private static AtomicInteger intGenerator = new AtomicInteger(0);
+
+ private final String projectRelativePath;
+ private final String moduleRelativePath;
+ private final String projectKey;
+ private final String language;
+ private final Type type;
+ private final Path absolutePath;
+ private final SensorStrategy sensorStrategy;
+
+ /**
+ * Testing purposes only!
+ */
+ public DefaultIndexedFile(String projectKey, Path baseDir, String relativePath, @Nullable String language) {
+ this(baseDir.resolve(relativePath), projectKey, relativePath, relativePath, Type.MAIN, language, intGenerator.getAndIncrement(),
+ new SensorStrategy());
+ }
+
+ public DefaultIndexedFile(Path absolutePath, String projectKey, String projectRelativePath, String moduleRelativePath, Type type, @Nullable String language, int batchId,
+ SensorStrategy sensorStrategy) {
+ super(batchId);
+ this.projectKey = projectKey;
+ this.projectRelativePath = PathUtils.sanitize(projectRelativePath);
+ this.moduleRelativePath = PathUtils.sanitize(moduleRelativePath);
+ this.type = type;
+ this.language = language;
+ this.sensorStrategy = sensorStrategy;
+ this.absolutePath = absolutePath;
+ }
+
+ @Override
+ public String relativePath() {
+ return sensorStrategy.isGlobal() ? projectRelativePath : moduleRelativePath;
+ }
+
+ public String getModuleRelativePath() {
+ return moduleRelativePath;
+ }
+
+ public String getProjectRelativePath() {
+ return projectRelativePath;
+ }
+
+ @Override
+ public String absolutePath() {
+ return PathUtils.sanitize(path().toString());
+ }
+
+ @Override
+ public File file() {
+ return path().toFile();
+ }
+
+ @Override
+ public Path path() {
+ return absolutePath;
+ }
+
+ @Override
+ public InputStream inputStream() throws IOException {
+ return Files.newInputStream(path());
+ }
+
+ @CheckForNull
+ @Override
+ public String language() {
+ return language;
+ }
+
+ @Override
+ public Type type() {
+ return type;
+ }
+
+ /**
+ * Component key (without branch).
+ */
+ @Override
+ public String key() {
+ return new StringBuilder().append(projectKey).append(":").append(projectRelativePath).toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof DefaultIndexedFile)) {
+ return false;
+ }
+
+ DefaultIndexedFile that = (DefaultIndexedFile) o;
+ return projectRelativePath.equals(that.projectRelativePath);
+ }
+
+ @Override
+ public int hashCode() {
+ return projectRelativePath.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return projectRelativePath;
+ }
+
+ @Override
+ public boolean isFile() {
+ return true;
+ }
+
+ @Override
+ public String filename() {
+ return path().getFileName().toString();
+ }
+
+ @Override
+ public URI uri() {
+ return path().toUri();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputComponent.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputComponent.java
new file mode 100644
index 00000000000..14acba4e1fe
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputComponent.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.measure.Metric;
+
+/**
+ * @since 5.2
+ */
+public abstract class DefaultInputComponent implements InputComponent {
+ private int id;
+ private Set<String> storedMetricKeys = new HashSet<>();
+
+ public DefaultInputComponent(int scannerId) {
+ this.id = scannerId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || this.getClass() != o.getClass()) {
+ return false;
+ }
+
+ DefaultInputComponent that = (DefaultInputComponent) o;
+ return key().equals(that.key());
+ }
+
+ public int scannerId() {
+ return id;
+ }
+
+ @Override
+ public int hashCode() {
+ return key().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "[key=" + key() + "]";
+ }
+
+ public void setHasMeasureFor(Metric metric) {
+ storedMetricKeys.add(metric.key());
+ }
+
+ public boolean hasMeasureFor(Metric metric) {
+ return storedMetricKeys.contains(metric.key());
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputDir.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputDir.java
new file mode 100644
index 00000000000..53f1800c720
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputDir.java
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Path;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.batch.fs.InputDir;
+import org.sonar.api.utils.PathUtils;
+
+/**
+ * @since 4.5
+ */
+public class DefaultInputDir extends DefaultInputComponent implements InputDir {
+
+ private final String relativePath;
+ private final String moduleKey;
+ private Path moduleBaseDir;
+
+ public DefaultInputDir(String moduleKey, String relativePath) {
+ super(-1);
+ this.moduleKey = moduleKey;
+ this.relativePath = PathUtils.sanitize(relativePath);
+ }
+
+ @Override
+ public String relativePath() {
+ return relativePath;
+ }
+
+ @Override
+ public String absolutePath() {
+ return PathUtils.sanitize(path().toString());
+ }
+
+ @Override
+ public File file() {
+ return path().toFile();
+ }
+
+ @Override
+ public Path path() {
+ if (moduleBaseDir == null) {
+ throw new IllegalStateException("Can not return the java.nio.file.Path because module baseDir is not set (see method setModuleBaseDir(java.io.File))");
+ }
+ return moduleBaseDir.resolve(relativePath);
+ }
+
+ public String moduleKey() {
+ return moduleKey;
+ }
+
+ @Override
+ public String key() {
+ StringBuilder sb = new StringBuilder().append(moduleKey).append(":");
+ if (StringUtils.isEmpty(relativePath)) {
+ sb.append("/");
+ } else {
+ sb.append(relativePath);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * For testing purpose. Will be automatically set when dir is added to {@link DefaultFileSystem}
+ */
+ public DefaultInputDir setModuleBaseDir(Path moduleBaseDir) {
+ this.moduleBaseDir = moduleBaseDir.normalize();
+ return this;
+ }
+
+ @Override
+ public boolean isFile() {
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || this.getClass() != o.getClass()) {
+ return false;
+ }
+
+ DefaultInputDir that = (DefaultInputDir) o;
+ return moduleKey.equals(that.moduleKey) && relativePath.equals(that.relativePath);
+ }
+
+ @Override
+ public int hashCode() {
+ return moduleKey.hashCode() + relativePath.hashCode() * 13;
+ }
+
+ @Override
+ public String toString() {
+ return "[moduleKey=" + moduleKey + ", relative=" + relativePath + ", basedir=" + moduleBaseDir + "]";
+ }
+
+ @Override
+ public URI uri() {
+ return path().toUri();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputFile.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputFile.java
new file mode 100644
index 00000000000..61561b0541a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputFile.java
@@ -0,0 +1,439 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.ByteOrderMark;
+import org.apache.commons.io.input.BOMInputStream;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextPointer;
+import org.sonar.api.batch.fs.TextRange;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+/**
+ * @since 4.2
+ * To create {@link InputFile} in tests, use TestInputFileBuilder.
+ */
+public class DefaultInputFile extends DefaultInputComponent implements InputFile {
+
+ private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+ private final DefaultIndexedFile indexedFile;
+ private final String contents;
+ private final Consumer<DefaultInputFile> metadataGenerator;
+
+ private boolean published;
+ private boolean excludedForCoverage;
+ private boolean excludedForDuplication;
+ private boolean ignoreAllIssues;
+ // Lazy init to save memory
+ private BitSet noSonarLines;
+ private Status status;
+ private Charset charset;
+ private Metadata metadata;
+ private Collection<int[]> ignoreIssuesOnlineRanges;
+ private BitSet executableLines;
+
+ public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator) {
+ this(indexedFile, metadataGenerator, null);
+ }
+
+ // For testing
+ public DefaultInputFile(DefaultIndexedFile indexedFile, Consumer<DefaultInputFile> metadataGenerator, @Nullable String contents) {
+ super(indexedFile.scannerId());
+ this.indexedFile = indexedFile;
+ this.metadataGenerator = metadataGenerator;
+ this.metadata = null;
+ this.published = false;
+ this.excludedForCoverage = false;
+ this.contents = contents;
+ }
+
+ public void checkMetadata() {
+ if (metadata == null) {
+ metadataGenerator.accept(this);
+ }
+ }
+
+ @Override
+ public InputStream inputStream() throws IOException {
+ return contents != null ? new ByteArrayInputStream(contents.getBytes(charset()))
+ : new BOMInputStream(Files.newInputStream(path()),
+ ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE);
+ }
+
+ @Override
+ public String contents() throws IOException {
+ if (contents != null) {
+ return contents;
+ } else {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ try (InputStream inputStream = inputStream()) {
+ byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+ int length;
+ while ((length = inputStream.read(buffer)) != -1) {
+ result.write(buffer, 0, length);
+ }
+ }
+ return result.toString(charset().name());
+ }
+ }
+
+ public DefaultInputFile setPublished(boolean published) {
+ this.published = published;
+ return this;
+ }
+
+ public boolean isPublished() {
+ return published;
+ }
+
+ public DefaultInputFile setExcludedForCoverage(boolean excludedForCoverage) {
+ this.excludedForCoverage = excludedForCoverage;
+ return this;
+ }
+
+ public boolean isExcludedForCoverage() {
+ return excludedForCoverage;
+ }
+
+ public DefaultInputFile setExcludedForDuplication(boolean excludedForDuplication) {
+ this.excludedForDuplication = excludedForDuplication;
+ return this;
+ }
+
+ public boolean isExcludedForDuplication() {
+ return excludedForDuplication;
+ }
+
+ /**
+ * @deprecated since 6.6
+ */
+ @Deprecated
+ @Override
+ public String relativePath() {
+ return indexedFile.relativePath();
+ }
+
+ public String getModuleRelativePath() {
+ return indexedFile.getModuleRelativePath();
+ }
+
+ public String getProjectRelativePath() {
+ return indexedFile.getProjectRelativePath();
+ }
+
+ @Override
+ public String absolutePath() {
+ return indexedFile.absolutePath();
+ }
+
+ @Override
+ public File file() {
+ return indexedFile.file();
+ }
+
+ @Override
+ public Path path() {
+ return indexedFile.path();
+ }
+
+ @CheckForNull
+ @Override
+ public String language() {
+ return indexedFile.language();
+ }
+
+ @Override
+ public Type type() {
+ return indexedFile.type();
+ }
+
+ /**
+ * Component key (without branch).
+ */
+ @Override
+ public String key() {
+ return indexedFile.key();
+ }
+
+ @Override
+ public int hashCode() {
+ return indexedFile.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return indexedFile.toString();
+ }
+
+ /**
+ * {@link #setStatus(Status)}
+ */
+ @Override
+ public Status status() {
+ checkMetadata();
+ return status;
+ }
+
+ @Override
+ public int lines() {
+ checkMetadata();
+ return metadata.lines();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ checkMetadata();
+ return metadata.isEmpty();
+ }
+
+ @Override
+ public Charset charset() {
+ checkMetadata();
+ return charset;
+ }
+
+ public int lastValidOffset() {
+ checkMetadata();
+ return metadata.lastValidOffset();
+ }
+
+ /**
+ * Digest hash of the file.
+ */
+ public String hash() {
+ checkMetadata();
+ return metadata.hash();
+ }
+
+ public int nonBlankLines() {
+ checkMetadata();
+ return metadata.nonBlankLines();
+ }
+
+ public int[] originalLineStartOffsets() {
+ checkMetadata();
+ checkState(metadata.originalLineStartOffsets() != null, "InputFile is not properly initialized.");
+ checkState(metadata.originalLineStartOffsets().length == metadata.lines(),
+ "InputFile is not properly initialized. 'originalLineStartOffsets' property length should be equal to 'lines'");
+ return metadata.originalLineStartOffsets();
+ }
+
+ public int[] originalLineEndOffsets() {
+ checkMetadata();
+ checkState(metadata.originalLineEndOffsets() != null, "InputFile is not properly initialized.");
+ checkState(metadata.originalLineEndOffsets().length == metadata.lines(),
+ "InputFile is not properly initialized. 'originalLineEndOffsets' property length should be equal to 'lines'");
+ return metadata.originalLineEndOffsets();
+ }
+
+ @Override
+ public TextPointer newPointer(int line, int lineOffset) {
+ checkMetadata();
+ DefaultTextPointer textPointer = new DefaultTextPointer(line, lineOffset);
+ checkValid(textPointer, "pointer");
+ return textPointer;
+ }
+
+ @Override
+ public TextRange newRange(TextPointer start, TextPointer end) {
+ checkMetadata();
+ checkValid(start, "start pointer");
+ checkValid(end, "end pointer");
+ return newRangeValidPointers(start, end, false);
+ }
+
+ @Override
+ public TextRange newRange(int startLine, int startLineOffset, int endLine, int endLineOffset) {
+ checkMetadata();
+ TextPointer start = newPointer(startLine, startLineOffset);
+ TextPointer end = newPointer(endLine, endLineOffset);
+ return newRangeValidPointers(start, end, false);
+ }
+
+ @Override
+ public TextRange selectLine(int line) {
+ checkMetadata();
+ TextPointer startPointer = newPointer(line, 0);
+ TextPointer endPointer = newPointer(line, lineLength(line));
+ return newRangeValidPointers(startPointer, endPointer, true);
+ }
+
+ public void validate(TextRange range) {
+ checkMetadata();
+ checkValid(range.start(), "start pointer");
+ checkValid(range.end(), "end pointer");
+ }
+
+ /**
+ * Create Range from global offsets. Used for backward compatibility with older API.
+ */
+ public TextRange newRange(int startOffset, int endOffset) {
+ checkMetadata();
+ return newRangeValidPointers(newPointer(startOffset), newPointer(endOffset), false);
+ }
+
+ public TextPointer newPointer(int globalOffset) {
+ checkMetadata();
+ checkArgument(globalOffset >= 0, "%s is not a valid offset for a file", globalOffset);
+ checkArgument(globalOffset <= lastValidOffset(), "%s is not a valid offset for file %s. Max offset is %s", globalOffset, this, lastValidOffset());
+ int line = findLine(globalOffset);
+ int startLineOffset = originalLineStartOffsets()[line - 1];
+ // In case the global offset is between \r and \n, move the pointer to a valid location
+ return new DefaultTextPointer(line, Math.min(globalOffset, originalLineEndOffsets()[line - 1]) - startLineOffset);
+ }
+
+ public DefaultInputFile setStatus(Status status) {
+ this.status = status;
+ return this;
+ }
+
+ public DefaultInputFile setCharset(Charset charset) {
+ this.charset = charset;
+ return this;
+ }
+
+ private void checkValid(TextPointer pointer, String owner) {
+ checkArgument(pointer.line() >= 1, "%s is not a valid line for a file", pointer.line());
+ checkArgument(pointer.line() <= this.metadata.lines(), "%s is not a valid line for %s. File %s has %s line(s)", pointer.line(), owner, this, metadata.lines());
+ checkArgument(pointer.lineOffset() >= 0, "%s is not a valid line offset for a file", pointer.lineOffset());
+ int lineLength = lineLength(pointer.line());
+ checkArgument(pointer.lineOffset() <= lineLength,
+ "%s is not a valid line offset for %s. File %s has %s character(s) at line %s", pointer.lineOffset(), owner, this, lineLength, pointer.line());
+ }
+
+ private int lineLength(int line) {
+ return originalLineEndOffsets()[line - 1] - originalLineStartOffsets()[line - 1];
+ }
+
+ private static TextRange newRangeValidPointers(TextPointer start, TextPointer end, boolean acceptEmptyRange) {
+ checkArgument(acceptEmptyRange ? (start.compareTo(end) <= 0) : (start.compareTo(end) < 0),
+ "Start pointer %s should be before end pointer %s", start, end);
+ return new DefaultTextRange(start, end);
+ }
+
+ private int findLine(int globalOffset) {
+ return Math.abs(Arrays.binarySearch(originalLineStartOffsets(), globalOffset) + 1);
+ }
+
+ public DefaultInputFile setMetadata(Metadata metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (this.getClass() != obj.getClass()) {
+ return false;
+ }
+
+ DefaultInputFile that = (DefaultInputFile) obj;
+ return this.getProjectRelativePath().equals(that.getProjectRelativePath());
+ }
+
+ @Override
+ public boolean isFile() {
+ return true;
+ }
+
+ @Override
+ public String filename() {
+ return indexedFile.filename();
+ }
+
+ @Override
+ public URI uri() {
+ return indexedFile.uri();
+ }
+
+ public void noSonarAt(Set<Integer> noSonarLines) {
+ if (this.noSonarLines == null) {
+ this.noSonarLines = new BitSet(lines());
+ }
+ noSonarLines.forEach(l -> this.noSonarLines.set(l - 1));
+ }
+
+ public boolean hasNoSonarAt(int line) {
+ if (this.noSonarLines == null) {
+ return false;
+ }
+ return this.noSonarLines.get(line - 1);
+ }
+
+ public boolean isIgnoreAllIssues() {
+ return ignoreAllIssues;
+ }
+
+ public void setIgnoreAllIssues(boolean ignoreAllIssues) {
+ this.ignoreAllIssues = ignoreAllIssues;
+ }
+
+ public void addIgnoreIssuesOnLineRanges(Collection<int[]> lineRanges) {
+ if (this.ignoreIssuesOnlineRanges == null) {
+ this.ignoreIssuesOnlineRanges = new ArrayList<>();
+ }
+ this.ignoreIssuesOnlineRanges.addAll(lineRanges);
+ }
+
+ public boolean isIgnoreAllIssuesOnLine(@Nullable Integer line) {
+ if (line == null || ignoreIssuesOnlineRanges == null) {
+ return false;
+ }
+ return ignoreIssuesOnlineRanges.stream().anyMatch(r -> r[0] <= line && line <= r[1]);
+ }
+
+ public void setExecutableLines(Set<Integer> executableLines) {
+ checkState(this.executableLines == null, "Executable lines have already been saved for file: {}", this.toString());
+ this.executableLines = new BitSet(lines());
+ executableLines.forEach(l -> this.executableLines.set(l - 1));
+ }
+
+ public Optional<Set<Integer>> getExecutableLines() {
+ if (this.executableLines == null) {
+ return Optional.empty();
+ }
+ return Optional.of(this.executableLines.stream().map(i -> i + 1).boxed().collect(Collectors.toSet()));
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputModule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputModule.java
new file mode 100644
index 00000000000..476a719da81
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputModule.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.InputModule;
+import org.sonar.api.scan.filesystem.PathResolver;
+
+import static org.sonar.api.impl.config.MultivalueProperty.parseAsCsv;
+
+@Immutable
+public class DefaultInputModule extends AbstractProjectOrModule implements InputModule {
+
+ private final List<Path> sourceDirsOrFiles;
+ private final List<Path> testDirsOrFiles;
+
+ /**
+ * For testing only!
+ */
+ public DefaultInputModule(ProjectDefinition definition) {
+ this(definition, 0);
+ }
+
+ public DefaultInputModule(ProjectDefinition definition, int scannerComponentId) {
+ super(definition, scannerComponentId);
+
+ this.sourceDirsOrFiles = initSources(definition, ProjectDefinition.SOURCES_PROPERTY);
+ this.testDirsOrFiles = initSources(definition, ProjectDefinition.TESTS_PROPERTY);
+ }
+
+ @CheckForNull
+ private List<Path> initSources(ProjectDefinition module, String propertyKey) {
+ if (!module.properties().containsKey(propertyKey)) {
+ return null;
+ }
+ List<Path> result = new ArrayList<>();
+ PathResolver pathResolver = new PathResolver();
+ String srcPropValue = module.properties().get(propertyKey);
+ if (srcPropValue != null) {
+ for (String sourcePath : parseAsCsv(propertyKey, srcPropValue)) {
+ File dirOrFile = pathResolver.relativeFile(getBaseDir().toFile(), sourcePath);
+ if (dirOrFile.exists()) {
+ result.add(dirOrFile.toPath());
+ }
+ }
+ }
+ return result;
+ }
+
+ public Optional<List<Path>> getSourceDirsOrFiles() {
+ return Optional.ofNullable(sourceDirsOrFiles);
+ }
+
+ public Optional<List<Path>> getTestDirsOrFiles() {
+ return Optional.ofNullable(testDirsOrFiles);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputProject.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputProject.java
new file mode 100644
index 00000000000..0ed150065ed
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultInputProject.java
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.scanner.fs.InputProject;
+
+@Immutable
+public class DefaultInputProject extends AbstractProjectOrModule implements InputProject {
+
+ /**
+ * For testing only!
+ */
+ public DefaultInputProject(ProjectDefinition definition) {
+ super(definition, 0);
+ }
+
+ public DefaultInputProject(ProjectDefinition definition, int scannerComponentId) {
+ super(definition, scannerComponentId);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextPointer.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextPointer.java
new file mode 100644
index 00000000000..541ee958bba
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextPointer.java
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import org.sonar.api.batch.fs.TextPointer;
+
+/**
+ * @since 5.2
+ */
+public class DefaultTextPointer implements TextPointer {
+
+ private final int line;
+ private final int lineOffset;
+
+ public DefaultTextPointer(int line, int lineOffset) {
+ this.line = line;
+ this.lineOffset = lineOffset;
+ }
+
+ @Override
+ public int line() {
+ return line;
+ }
+
+ @Override
+ public int lineOffset() {
+ return lineOffset;
+ }
+
+ @Override
+ public String toString() {
+ return "[line=" + line + ", lineOffset=" + lineOffset + "]";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof DefaultTextPointer)) {
+ return false;
+ }
+ DefaultTextPointer other = (DefaultTextPointer) obj;
+ return other.line == this.line && other.lineOffset == this.lineOffset;
+ }
+
+ @Override
+ public int hashCode() {
+ return 37 * this.line + lineOffset;
+ }
+
+ @Override
+ public int compareTo(TextPointer o) {
+ if (this.line == o.line()) {
+ return Integer.compare(this.lineOffset, o.lineOffset());
+ }
+ return Integer.compare(this.line, o.line());
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextRange.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextRange.java
new file mode 100644
index 00000000000..0bf0e5151ab
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/DefaultTextRange.java
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import org.sonar.api.batch.fs.TextPointer;
+import org.sonar.api.batch.fs.TextRange;
+
+/**
+ * @since 5.2
+ */
+public class DefaultTextRange implements TextRange {
+
+ private final TextPointer start;
+ private final TextPointer end;
+
+ public DefaultTextRange(TextPointer start, TextPointer end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ @Override
+ public TextPointer start() {
+ return start;
+ }
+
+ @Override
+ public TextPointer end() {
+ return end;
+ }
+
+ @Override
+ public boolean overlap(TextRange another) {
+ // [A,B] and [C,D]
+ // B > C && D > A
+ return this.end.compareTo(another.start()) > 0 && another.end().compareTo(this.start) > 0;
+ }
+
+ @Override
+ public String toString() {
+ return "Range[from " + start + " to " + end + "]";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof DefaultTextRange)) {
+ return false;
+ }
+ DefaultTextRange other = (DefaultTextRange) obj;
+ return start.equals(other.start) && end.equals(other.end);
+ }
+
+ @Override
+ public int hashCode() {
+ return start.hashCode() * 17 + end.hashCode();
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/FileMetadata.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/FileMetadata.java
new file mode 100644
index 00000000000..8078db10b67
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/FileMetadata.java
@@ -0,0 +1,161 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.impl.fs.charhandler.CharHandler;
+import org.sonar.api.impl.fs.charhandler.FileHashComputer;
+import org.sonar.api.impl.fs.charhandler.LineCounter;
+import org.sonar.api.impl.fs.charhandler.LineHashComputer;
+import org.sonar.api.impl.fs.charhandler.LineOffsetCounter;
+
+/**
+ * Computes hash of files. Ends of Lines are ignored, so files with
+ * same content but different EOL encoding have the same hash.
+ */
+@Immutable
+public class FileMetadata {
+ private static final char LINE_FEED = '\n';
+ private static final char CARRIAGE_RETURN = '\r';
+
+ /**
+ * Compute hash of a file ignoring line ends differences.
+ * Maximum performance is needed.
+ */
+ public Metadata readMetadata(InputStream stream, Charset encoding, String filePath, @Nullable CharHandler otherHandler) {
+ LineCounter lineCounter = new LineCounter(filePath, encoding);
+ FileHashComputer fileHashComputer = new FileHashComputer(filePath);
+ LineOffsetCounter lineOffsetCounter = new LineOffsetCounter();
+
+ if (otherHandler != null) {
+ CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter, otherHandler};
+ readFile(stream, encoding, filePath, handlers);
+ } else {
+ CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter};
+ readFile(stream, encoding, filePath, handlers);
+ }
+ return new Metadata(lineCounter.lines(), lineCounter.nonBlankLines(), fileHashComputer.getHash(), lineOffsetCounter.getOriginalLineStartOffsets(),
+ lineOffsetCounter.getOriginalLineEndOffsets(),
+ lineOffsetCounter.getLastValidOffset());
+ }
+
+ public Metadata readMetadata(InputStream stream, Charset encoding, String filePath) {
+ return readMetadata(stream, encoding, filePath, null);
+ }
+
+ /**
+ * For testing purpose
+ */
+ public Metadata readMetadata(Reader reader) {
+ LineCounter lineCounter = new LineCounter("fromString", StandardCharsets.UTF_16);
+ FileHashComputer fileHashComputer = new FileHashComputer("fromString");
+ LineOffsetCounter lineOffsetCounter = new LineOffsetCounter();
+ CharHandler[] handlers = {lineCounter, fileHashComputer, lineOffsetCounter};
+
+ try {
+ read(reader, handlers);
+ } catch (IOException e) {
+ throw new IllegalStateException("Should never occur", e);
+ }
+ return new Metadata(lineCounter.lines(), lineCounter.nonBlankLines(), fileHashComputer.getHash(), lineOffsetCounter.getOriginalLineStartOffsets(),
+ lineOffsetCounter.getOriginalLineEndOffsets(),
+ lineOffsetCounter.getLastValidOffset());
+ }
+
+ public static void readFile(InputStream stream, Charset encoding, String filePath, CharHandler[] handlers) {
+ try (Reader reader = new BufferedReader(new InputStreamReader(stream, encoding))) {
+ read(reader, handlers);
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Fail to read file '%s' with encoding '%s'", filePath, encoding), e);
+ }
+ }
+
+ private static void read(Reader reader, CharHandler[] handlers) throws IOException {
+ char c;
+ int i = reader.read();
+ boolean afterCR = false;
+ while (i != -1) {
+ c = (char) i;
+ if (afterCR) {
+ for (CharHandler handler : handlers) {
+ if (c == CARRIAGE_RETURN) {
+ handler.newLine();
+ handler.handleAll(c);
+ } else if (c == LINE_FEED) {
+ handler.handleAll(c);
+ handler.newLine();
+ } else {
+ handler.newLine();
+ handler.handleIgnoreEoL(c);
+ handler.handleAll(c);
+ }
+ }
+ afterCR = c == CARRIAGE_RETURN;
+ } else if (c == LINE_FEED) {
+ for (CharHandler handler : handlers) {
+ handler.handleAll(c);
+ handler.newLine();
+ }
+ } else if (c == CARRIAGE_RETURN) {
+ afterCR = true;
+ for (CharHandler handler : handlers) {
+ handler.handleAll(c);
+ }
+ } else {
+ for (CharHandler handler : handlers) {
+ handler.handleIgnoreEoL(c);
+ handler.handleAll(c);
+ }
+ }
+ i = reader.read();
+ }
+ for (CharHandler handler : handlers) {
+ if (afterCR) {
+ handler.newLine();
+ }
+ handler.eof();
+ }
+ }
+
+ @FunctionalInterface
+ public interface LineHashConsumer {
+ void consume(int lineIdx, @Nullable byte[] hash);
+ }
+
+ /**
+ * Compute a MD5 hash of each line of the file after removing of all blank chars
+ */
+ public static void computeLineHashesForIssueTracking(InputFile f, LineHashConsumer consumer) {
+ try {
+ readFile(f.inputStream(), f.charset(), f.absolutePath(), new CharHandler[] {new LineHashComputer(consumer, f.file())});
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to compute line hashes for " + f.absolutePath(), e);
+ }
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/Metadata.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/Metadata.java
new file mode 100644
index 00000000000..e17704135ce
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/Metadata.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.util.Arrays;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public class Metadata {
+ private final int lines;
+ private final int nonBlankLines;
+ private final String hash;
+ private final int[] originalLineStartOffsets;
+ private final int[] originalLineEndOffsets;
+ private final int lastValidOffset;
+
+ public Metadata(int lines, int nonBlankLines, String hash, int[] originalLineStartOffsets, int[] originalLineEndOffsets, int lastValidOffset) {
+ this.lines = lines;
+ this.nonBlankLines = nonBlankLines;
+ this.hash = hash;
+ this.originalLineStartOffsets = Arrays.copyOf(originalLineStartOffsets, originalLineStartOffsets.length);
+ this.originalLineEndOffsets = Arrays.copyOf(originalLineEndOffsets, originalLineEndOffsets.length);
+ this.lastValidOffset = lastValidOffset;
+ }
+
+ public int lines() {
+ return lines;
+ }
+
+ public int nonBlankLines() {
+ return nonBlankLines;
+ }
+
+ public String hash() {
+ return hash;
+ }
+
+ public int[] originalLineStartOffsets() {
+ return originalLineStartOffsets;
+ }
+
+ public int[] originalLineEndOffsets() {
+ return originalLineEndOffsets;
+ }
+
+ public int lastValidOffset() {
+ return lastValidOffset;
+ }
+
+ public boolean isEmpty() {
+ return lastValidOffset == 0;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/PathPattern.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/PathPattern.java
new file mode 100644
index 00000000000..30a813d5a60
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/PathPattern.java
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.nio.file.Path;
+import javax.annotation.concurrent.ThreadSafe;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.PathUtils;
+import org.sonar.api.utils.WildcardPattern;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+@ThreadSafe
+public abstract class PathPattern {
+
+ private static final Logger LOG = Loggers.get(PathPattern.class);
+
+ /**
+ * @deprecated since 6.6
+ */
+ @Deprecated
+ private static final String ABSOLUTE_PATH_PATTERN_PREFIX = "file:";
+ final WildcardPattern pattern;
+
+ PathPattern(String pattern) {
+ this.pattern = WildcardPattern.create(pattern);
+ }
+
+ public abstract boolean match(Path absolutePath, Path relativePath);
+
+ public abstract boolean match(Path absolutePath, Path relativePath, boolean caseSensitiveFileExtension);
+
+ public static PathPattern create(String s) {
+ String trimmed = StringUtils.trim(s);
+ if (StringUtils.startsWithIgnoreCase(trimmed, ABSOLUTE_PATH_PATTERN_PREFIX)) {
+ LOG.warn("Using absolute path pattern is deprecated. Please use relative path instead of '" + trimmed + "'");
+ return new AbsolutePathPattern(StringUtils.substring(trimmed, ABSOLUTE_PATH_PATTERN_PREFIX.length()));
+ }
+ return new RelativePathPattern(trimmed);
+ }
+
+ public static PathPattern[] create(String[] s) {
+ PathPattern[] result = new PathPattern[s.length];
+ for (int i = 0; i < s.length; i++) {
+ result[i] = create(s[i]);
+ }
+ return result;
+ }
+
+ /**
+ * @deprecated since 6.6
+ */
+ @Deprecated
+ private static class AbsolutePathPattern extends PathPattern {
+ private AbsolutePathPattern(String pattern) {
+ super(pattern);
+ }
+
+ @Override
+ public boolean match(Path absolutePath, Path relativePath) {
+ return match(absolutePath, relativePath, true);
+ }
+
+ @Override
+ public boolean match(Path absolutePath, Path relativePath, boolean caseSensitiveFileExtension) {
+ String path = PathUtils.sanitize(absolutePath.toString());
+ if (!caseSensitiveFileExtension) {
+ String extension = sanitizeExtension(FilenameUtils.getExtension(path));
+ if (StringUtils.isNotBlank(extension)) {
+ path = StringUtils.removeEndIgnoreCase(path, extension);
+ path = path + extension;
+ }
+ }
+ return pattern.match(path);
+ }
+
+ @Override
+ public String toString() {
+ return ABSOLUTE_PATH_PATTERN_PREFIX + pattern.toString();
+ }
+ }
+
+ /**
+ * Path relative to module basedir
+ */
+ private static class RelativePathPattern extends PathPattern {
+ private RelativePathPattern(String pattern) {
+ super(pattern);
+ }
+
+ @Override
+ public boolean match(Path absolutePath, Path relativePath) {
+ return match(absolutePath, relativePath, true);
+ }
+
+ @Override
+ public boolean match(Path absolutePath, Path relativePath, boolean caseSensitiveFileExtension) {
+ String path = PathUtils.sanitize(relativePath.toString());
+ if (!caseSensitiveFileExtension) {
+ String extension = sanitizeExtension(FilenameUtils.getExtension(path));
+ if (StringUtils.isNotBlank(extension)) {
+ path = StringUtils.removeEndIgnoreCase(path, extension);
+ path = path + extension;
+ }
+ }
+ return path != null && pattern.match(path);
+ }
+
+ @Override
+ public String toString() {
+ return pattern.toString();
+ }
+ }
+
+ static String sanitizeExtension(String suffix) {
+ return StringUtils.lowerCase(StringUtils.removeStart(suffix, "."));
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/SensorStrategy.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/SensorStrategy.java
new file mode 100644
index 00000000000..d956d061f91
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/SensorStrategy.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * A shared, mutable object in the project container.
+ * It's used during the execution of sensors to decide whether
+ * sensors should be executed once for the entire project, or per-module.
+ * It is also injected into each InputFile to change the behavior of {@link InputFile#relativePath()}
+ */
+public class SensorStrategy {
+
+ private boolean global = true;
+
+ public boolean isGlobal() {
+ return global;
+ }
+
+ public void setGlobal(boolean global) {
+ this.global = global;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/TestInputFileBuilder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/TestInputFileBuilder.java
new file mode 100644
index 00000000000..e731e7ff7ab
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/TestInputFileBuilder.java
@@ -0,0 +1,278 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.utils.PathUtils;
+
+/**
+ * Intended to be used in unit tests that need to create {@link InputFile}s.
+ * An InputFile is unambiguously identified by a <b>module key</b> and a <b>relative path</b>, so these parameters are mandatory.
+ * <p>
+ * A module base directory is only needed to construct absolute paths.
+ * <p>
+ * Examples of usage of the constructors:
+ *
+ * <pre>
+ * InputFile file1 = TestInputFileBuilder.create("module1", "myfile.java").build();
+ * InputFile file2 = TestInputFileBuilder.create("", fs.baseDir(), myfile).build();
+ * </pre>
+ * <p>
+ * file1 will have the "module1" as both module key and module base directory.
+ * file2 has an empty string as module key, and a relative path which is the path from the filesystem base directory to myfile.
+ *
+ * @since 6.3
+ */
+public class TestInputFileBuilder {
+ private static int batchId = 1;
+
+ private final int id;
+ private final String relativePath;
+ private final String projectKey;
+ @CheckForNull
+ private Path projectBaseDir;
+ private Path moduleBaseDir;
+ private String language;
+ private InputFile.Type type = InputFile.Type.MAIN;
+ private InputFile.Status status;
+ private int lines = -1;
+ private Charset charset;
+ private String hash;
+ private int nonBlankLines;
+ private int[] originalLineStartOffsets = new int[0];
+ private int[] originalLineEndOffsets = new int[0];
+ private int lastValidOffset = -1;
+ private boolean publish = true;
+ private String contents;
+
+ /**
+ * Create a InputFile identified by the given project key and relative path.
+ */
+ public TestInputFileBuilder(String projectKey, String relativePath) {
+ this(projectKey, relativePath, batchId++);
+ }
+
+ /**
+ * Create a InputFile with a given module key and module base directory.
+ * The relative path is generated comparing the file path to the module base directory.
+ * filePath must point to a file that is within the module base directory.
+ */
+ public TestInputFileBuilder(String projectKey, File moduleBaseDir, File filePath) {
+ String relativePath = moduleBaseDir.toPath().relativize(filePath.toPath()).toString();
+ this.projectKey = projectKey;
+ setModuleBaseDir(moduleBaseDir.toPath());
+ this.relativePath = PathUtils.sanitize(relativePath);
+ this.id = batchId++;
+ }
+
+ public TestInputFileBuilder(String projectKey, String relativePath, int id) {
+ this.projectKey = projectKey;
+ setModuleBaseDir(Paths.get(projectKey));
+ this.relativePath = PathUtils.sanitize(relativePath);
+ this.id = id;
+ }
+
+ public static TestInputFileBuilder create(String moduleKey, File moduleBaseDir, File filePath) {
+ return new TestInputFileBuilder(moduleKey, moduleBaseDir, filePath);
+ }
+
+ public static TestInputFileBuilder create(String moduleKey, String relativePath) {
+ return new TestInputFileBuilder(moduleKey, relativePath);
+ }
+
+ public static int nextBatchId() {
+ return batchId++;
+ }
+
+ public TestInputFileBuilder setProjectBaseDir(Path projectBaseDir) {
+ this.projectBaseDir = normalize(projectBaseDir);
+ return this;
+ }
+
+ public TestInputFileBuilder setModuleBaseDir(Path moduleBaseDir) {
+ this.moduleBaseDir = normalize(moduleBaseDir);
+ return this;
+ }
+
+ private static Path normalize(Path path) {
+ try {
+ return path.normalize().toRealPath(LinkOption.NOFOLLOW_LINKS);
+ } catch (IOException e) {
+ return path.normalize();
+ }
+ }
+
+ public TestInputFileBuilder setLanguage(@Nullable String language) {
+ this.language = language;
+ return this;
+ }
+
+ public TestInputFileBuilder setType(InputFile.Type type) {
+ this.type = type;
+ return this;
+ }
+
+ public TestInputFileBuilder setStatus(InputFile.Status status) {
+ this.status = status;
+ return this;
+ }
+
+ public TestInputFileBuilder setLines(int lines) {
+ this.lines = lines;
+ return this;
+ }
+
+ public TestInputFileBuilder setCharset(Charset charset) {
+ this.charset = charset;
+ return this;
+ }
+
+ public TestInputFileBuilder setHash(String hash) {
+ this.hash = hash;
+ return this;
+ }
+
+ /**
+ * Set contents of the file and calculates metadata from it.
+ * The contents will be returned by {@link InputFile#contents()} and {@link InputFile#inputStream()} and can be
+ * inconsistent with the actual physical file pointed by {@link InputFile#path()}, {@link InputFile#absolutePath()}, etc.
+ */
+ public TestInputFileBuilder setContents(String content) {
+ this.contents = content;
+ initMetadata(content);
+ return this;
+ }
+
+ public TestInputFileBuilder setNonBlankLines(int nonBlankLines) {
+ this.nonBlankLines = nonBlankLines;
+ return this;
+ }
+
+ public TestInputFileBuilder setLastValidOffset(int lastValidOffset) {
+ this.lastValidOffset = lastValidOffset;
+ return this;
+ }
+
+ public TestInputFileBuilder setOriginalLineStartOffsets(int[] originalLineStartOffsets) {
+ this.originalLineStartOffsets = originalLineStartOffsets;
+ return this;
+ }
+
+ public TestInputFileBuilder setOriginalLineEndOffsets(int[] originalLineEndOffsets) {
+ this.originalLineEndOffsets = originalLineEndOffsets;
+ return this;
+ }
+
+ public TestInputFileBuilder setPublish(boolean publish) {
+ this.publish = publish;
+ return this;
+ }
+
+ public TestInputFileBuilder setMetadata(Metadata metadata) {
+ this.setLines(metadata.lines());
+ this.setLastValidOffset(metadata.lastValidOffset());
+ this.setNonBlankLines(metadata.nonBlankLines());
+ this.setHash(metadata.hash());
+ this.setOriginalLineStartOffsets(metadata.originalLineStartOffsets());
+ this.setOriginalLineEndOffsets(metadata.originalLineEndOffsets());
+ return this;
+ }
+
+ public TestInputFileBuilder initMetadata(String content) {
+ return setMetadata(new FileMetadata().readMetadata(new StringReader(content)));
+ }
+
+ public DefaultInputFile build() {
+ Path absolutePath = moduleBaseDir.resolve(relativePath);
+ if (projectBaseDir == null) {
+ projectBaseDir = moduleBaseDir;
+ }
+ String projectRelativePath = projectBaseDir.relativize(absolutePath).toString();
+ DefaultIndexedFile indexedFile = new DefaultIndexedFile(absolutePath, projectKey, projectRelativePath, relativePath, type, language, id, new SensorStrategy());
+ DefaultInputFile inputFile = new DefaultInputFile(indexedFile,
+ f -> f.setMetadata(new Metadata(lines, nonBlankLines, hash, originalLineStartOffsets, originalLineEndOffsets, lastValidOffset)),
+ contents);
+ inputFile.setStatus(status);
+ inputFile.setCharset(charset);
+ inputFile.setPublished(publish);
+ return inputFile;
+ }
+
+ public static DefaultInputModule newDefaultInputModule(String moduleKey, File baseDir) {
+ ProjectDefinition definition = ProjectDefinition.create()
+ .setKey(moduleKey)
+ .setBaseDir(baseDir)
+ .setWorkDir(new File(baseDir, ".sonar"));
+ return newDefaultInputModule(definition);
+ }
+
+ public static DefaultInputModule newDefaultInputModule(ProjectDefinition projectDefinition) {
+ return new DefaultInputModule(projectDefinition, TestInputFileBuilder.nextBatchId());
+ }
+
+ public static DefaultInputModule newDefaultInputModule(AbstractProjectOrModule parent, String key) throws IOException {
+ Path basedir = parent.getBaseDir().resolve(key);
+ Files.createDirectory(basedir);
+ return newDefaultInputModule(key, basedir.toFile());
+ }
+
+ public static DefaultInputProject newDefaultInputProject(String projectKey, File baseDir) {
+ ProjectDefinition definition = ProjectDefinition.create()
+ .setKey(projectKey)
+ .setBaseDir(baseDir)
+ .setWorkDir(new File(baseDir, ".sonar"));
+ return newDefaultInputProject(definition);
+ }
+
+ public static DefaultInputProject newDefaultInputProject(ProjectDefinition projectDefinition) {
+ return new DefaultInputProject(projectDefinition, TestInputFileBuilder.nextBatchId());
+ }
+
+ public static DefaultInputProject newDefaultInputProject(String key, Path baseDir) throws IOException {
+ Files.createDirectory(baseDir);
+ return newDefaultInputProject(key, baseDir.toFile());
+ }
+
+ public static DefaultInputDir newDefaultInputDir(AbstractProjectOrModule module, String relativePath) throws IOException {
+ Path basedir = module.getBaseDir().resolve(relativePath);
+ Files.createDirectory(basedir);
+ return new DefaultInputDir(module.key(), relativePath)
+ .setModuleBaseDir(module.getBaseDir());
+ }
+
+ public static DefaultInputFile newDefaultInputFile(Path projectBaseDir, AbstractProjectOrModule module, String relativePath) {
+ return new TestInputFileBuilder(module.key(), relativePath)
+ .setStatus(InputFile.Status.SAME)
+ .setProjectBaseDir(projectBaseDir)
+ .setModuleBaseDir(module.getBaseDir())
+ .build();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/CharHandler.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/CharHandler.java
new file mode 100644
index 00000000000..06218a6a9f2
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/CharHandler.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.charhandler;
+
+public abstract class CharHandler {
+
+ public void handleAll(char c) {
+ }
+
+ public void handleIgnoreEoL(char c) {
+ }
+
+ public void newLine() {
+ }
+
+ public void eof() {
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/FileHashComputer.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/FileHashComputer.java
new file mode 100644
index 00000000000..eaa5672eef6
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/FileHashComputer.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.charhandler;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.DigestUtils;
+
+public class FileHashComputer extends CharHandler {
+ private static final char LINE_FEED = '\n';
+
+
+ private MessageDigest globalMd5Digest = DigestUtils.getMd5Digest();
+ private StringBuilder sb = new StringBuilder();
+ private final CharsetEncoder encoder;
+ private final String filePath;
+
+ public FileHashComputer(String filePath) {
+ encoder = StandardCharsets.UTF_8.newEncoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE);
+ this.filePath = filePath;
+ }
+
+ @Override
+ public void handleIgnoreEoL(char c) {
+ sb.append(c);
+ }
+
+ @Override
+ public void newLine() {
+ sb.append(LINE_FEED);
+ processBuffer();
+ sb.setLength(0);
+ }
+
+ @Override
+ public void eof() {
+ if (sb.length() > 0) {
+ processBuffer();
+ }
+ }
+
+ private void processBuffer() {
+ try {
+ if (sb.length() > 0) {
+ ByteBuffer encoded = encoder.encode(CharBuffer.wrap(sb));
+ globalMd5Digest.update(encoded.array(), 0, encoded.limit());
+ }
+ } catch (CharacterCodingException e) {
+ throw new IllegalStateException("Error encoding line hash in file: " + filePath, e);
+ }
+ }
+
+ public String getHash() {
+ return Hex.encodeHexString(globalMd5Digest.digest());
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/IntArrayList.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/IntArrayList.java
new file mode 100644
index 00000000000..b298b0524c5
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/IntArrayList.java
@@ -0,0 +1,117 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.charhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Specialization of {@link java.util.ArrayList} to create a list of int (only append elements) and then produce an int[].
+ */
+class IntArrayList {
+
+ /**
+ * Default initial capacity.
+ */
+ private static final int DEFAULT_CAPACITY = 10;
+
+ /**
+ * Shared empty array instance used for default sized empty instances. We
+ * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
+ * first element is added.
+ */
+ private static final int[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
+
+ /**
+ * The array buffer into which the elements of the ArrayList are stored.
+ * The capacity of the IntArrayList is the length of this array buffer. Any
+ * empty IntArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
+ * will be expanded to DEFAULT_CAPACITY when the first element is added.
+ */
+ private int[] elementData;
+
+ /**
+ * The size of the IntArrayList (the number of elements it contains).
+ */
+ private int size;
+
+ /**
+ * Constructs an empty list with an initial capacity of ten.
+ */
+ public IntArrayList() {
+ this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
+ }
+
+ /**
+ * Trims the capacity of this <tt>IntArrayList</tt> instance to be the
+ * list's current size and return the internal array. An application can use this operation to minimize
+ * the storage of an <tt>IntArrayList</tt> instance.
+ */
+ public int[] trimAndGet() {
+ if (size < elementData.length) {
+ elementData = Arrays.copyOf(elementData, size);
+ }
+ return elementData;
+ }
+
+ private void ensureCapacityInternal(int minCapacity) {
+ int capacity = minCapacity;
+ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
+ capacity = Math.max(DEFAULT_CAPACITY, minCapacity);
+ }
+
+ ensureExplicitCapacity(capacity);
+ }
+
+ private void ensureExplicitCapacity(int minCapacity) {
+ if (minCapacity - elementData.length > 0) {
+ grow(minCapacity);
+ }
+ }
+
+ /**
+ * Increases the capacity to ensure that it can hold at least the
+ * number of elements specified by the minimum capacity argument.
+ *
+ * @param minCapacity the desired minimum capacity
+ */
+ private void grow(int minCapacity) {
+ int oldCapacity = elementData.length;
+ int newCapacity = oldCapacity + (oldCapacity >> 1);
+ if (newCapacity - minCapacity < 0) {
+ newCapacity = minCapacity;
+ }
+ elementData = Arrays.copyOf(elementData, newCapacity);
+ }
+
+ /**
+ * Appends the specified element to the end of this list.
+ *
+ * @param e element to be appended to this list
+ * @return <tt>true</tt> (as specified by {@link Collection#add})
+ */
+ public boolean add(int e) {
+ ensureCapacityInternal(size + 1);
+ elementData[size] = e;
+ size++;
+ return true;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineCounter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineCounter.java
new file mode 100644
index 00000000000..ba5093b29e8
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineCounter.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.charhandler;
+
+import java.nio.charset.Charset;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+public class LineCounter extends CharHandler {
+ private static final Logger LOG = Loggers.get(LineCounter.class);
+
+ private int lines = 1;
+ private int nonBlankLines = 0;
+ private boolean blankLine = true;
+ boolean alreadyLoggedInvalidCharacter = false;
+ private final String filePath;
+ private final Charset encoding;
+
+ public LineCounter(String filePath, Charset encoding) {
+ this.filePath = filePath;
+ this.encoding = encoding;
+ }
+
+ @Override
+ public void handleAll(char c) {
+ if (!alreadyLoggedInvalidCharacter && c == '\ufffd') {
+ LOG.warn("Invalid character encountered in file {} at line {} for encoding {}. Please fix file content or configure the encoding to be used using property '{}'.", filePath,
+ lines, encoding, CoreProperties.ENCODING_PROPERTY);
+ alreadyLoggedInvalidCharacter = true;
+ }
+ }
+
+ @Override
+ public void newLine() {
+ lines++;
+ if (!blankLine) {
+ nonBlankLines++;
+ }
+ blankLine = true;
+ }
+
+ @Override
+ public void handleIgnoreEoL(char c) {
+ if (!Character.isWhitespace(c)) {
+ blankLine = false;
+ }
+ }
+
+ @Override
+ public void eof() {
+ if (!blankLine) {
+ nonBlankLines++;
+ }
+ }
+
+ public int lines() {
+ return lines;
+ }
+
+ public int nonBlankLines() {
+ return nonBlankLines;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineHashComputer.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineHashComputer.java
new file mode 100644
index 00000000000..8384258161c
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineHashComputer.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.charhandler;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.sonar.api.impl.fs.FileMetadata;
+
+public class LineHashComputer extends CharHandler {
+ private final MessageDigest lineMd5Digest = DigestUtils.getMd5Digest();
+ private final CharsetEncoder encoder;
+ private final StringBuilder sb = new StringBuilder();
+ private final FileMetadata.LineHashConsumer consumer;
+ private final File file;
+ private int line = 1;
+
+ public LineHashComputer(FileMetadata.LineHashConsumer consumer, File f) {
+ this.consumer = consumer;
+ this.file = f;
+ this.encoder = StandardCharsets.UTF_8.newEncoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE);
+ }
+
+ @Override
+ public void handleIgnoreEoL(char c) {
+ if (!Character.isWhitespace(c)) {
+ sb.append(c);
+ }
+ }
+
+ @Override
+ public void newLine() {
+ processBuffer();
+ sb.setLength(0);
+ line++;
+ }
+
+ @Override
+ public void eof() {
+ if (this.line > 0) {
+ processBuffer();
+ }
+ }
+
+ private void processBuffer() {
+ try {
+ if (sb.length() > 0) {
+ ByteBuffer encoded = encoder.encode(CharBuffer.wrap(sb));
+ lineMd5Digest.update(encoded.array(), 0, encoded.limit());
+ consumer.consume(line, lineMd5Digest.digest());
+ }
+ } catch (CharacterCodingException e) {
+ throw new IllegalStateException("Error encoding line hash in file: " + file.getAbsolutePath(), e);
+ }
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineOffsetCounter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineOffsetCounter.java
new file mode 100644
index 00000000000..1b0ad31fed1
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/LineOffsetCounter.java
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.charhandler;
+
+public class LineOffsetCounter extends CharHandler {
+ private long currentOriginalLineStartOffset = 0;
+ private long currentOriginalLineEndOffset = 0;
+ private final IntArrayList originalLineStartOffsets = new IntArrayList();
+ private final IntArrayList originalLineEndOffsets = new IntArrayList();
+ private long lastValidOffset = 0;
+
+ public LineOffsetCounter() {
+ originalLineStartOffsets.add(0);
+ }
+
+ @Override
+ public void handleAll(char c) {
+ currentOriginalLineStartOffset++;
+ }
+
+ @Override
+ public void handleIgnoreEoL(char c) {
+ currentOriginalLineEndOffset++;
+ }
+
+ @Override
+ public void newLine() {
+ if (currentOriginalLineStartOffset > Integer.MAX_VALUE) {
+ throw new IllegalStateException("File is too big: " + currentOriginalLineStartOffset);
+ }
+ originalLineStartOffsets.add((int) currentOriginalLineStartOffset);
+ originalLineEndOffsets.add((int) currentOriginalLineEndOffset);
+ currentOriginalLineEndOffset = currentOriginalLineStartOffset;
+ }
+
+ @Override
+ public void eof() {
+ originalLineEndOffsets.add((int) currentOriginalLineEndOffset);
+ lastValidOffset = currentOriginalLineStartOffset;
+ }
+
+ public int[] getOriginalLineStartOffsets() {
+ return originalLineStartOffsets.trimAndGet();
+ }
+
+ public int[] getOriginalLineEndOffsets() {
+ return originalLineEndOffsets.trimAndGet();
+ }
+
+ public int getLastValidOffset() {
+ if (lastValidOffset > Integer.MAX_VALUE) {
+ throw new IllegalStateException("File is too big: " + lastValidOffset);
+ }
+ return (int) lastValidOffset;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/package-info.java
new file mode 100644
index 00000000000..9c9bce61dbc
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/charhandler/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.fs.charhandler;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/package-info.java
new file mode 100644
index 00000000000..10a797893e5
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.fs;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbsolutePathPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbsolutePathPredicate.java
new file mode 100644
index 00000000000..c83cc6463a1
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbsolutePathPredicate.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.PathUtils;
+
+/**
+ * @since 4.2
+ */
+class AbsolutePathPredicate extends AbstractFilePredicate {
+
+ private final String path;
+ private final Path baseDir;
+
+ AbsolutePathPredicate(String path, Path baseDir) {
+ this.baseDir = baseDir;
+ this.path = PathUtils.sanitize(path);
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return path.equals(f.absolutePath());
+ }
+
+ @Override
+ public Iterable<InputFile> get(Index index) {
+ String relative = PathUtils.sanitize(new PathResolver().relativePath(baseDir.toFile(), new File(path)));
+ if (relative == null) {
+ return Collections.emptyList();
+ }
+ InputFile f = index.inputFile(relative);
+ return f != null ? Arrays.asList(f) : Collections.<InputFile>emptyList();
+ }
+
+ @Override
+ public int priority() {
+ return USE_INDEX;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbstractFilePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbstractFilePredicate.java
new file mode 100644
index 00000000000..1a964b49b6b
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AbstractFilePredicate.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.stream.StreamSupport;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * Partial implementation of {@link OptimizedFilePredicate}.
+ * @since 5.1
+ */
+public abstract class AbstractFilePredicate implements OptimizedFilePredicate {
+
+ protected static final int DEFAULT_PRIORITY = 10;
+ protected static final int USE_INDEX = 20;
+
+ @Override
+ public Iterable<InputFile> filter(Iterable<InputFile> target) {
+ return () -> StreamSupport.stream(target.spliterator(), false)
+ .filter(this::apply)
+ .iterator();
+ }
+
+ @Override
+ public Iterable<InputFile> get(Index index) {
+ return filter(index.inputFiles());
+ }
+
+ @Override
+ public int priority() {
+ return DEFAULT_PRIORITY;
+ }
+
+ @Override
+ public final int compareTo(OptimizedFilePredicate o) {
+ return o.priority() - priority();
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AndPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AndPredicate.java
new file mode 100644
index 00000000000..d2ea1f35f59
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/AndPredicate.java
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @since 4.2
+ */
+class AndPredicate extends AbstractFilePredicate implements OperatorPredicate {
+
+ private final List<OptimizedFilePredicate> predicates = new ArrayList<>();
+
+ private AndPredicate() {
+ }
+
+ public static FilePredicate create(Collection<FilePredicate> predicates) {
+ if (predicates.isEmpty()) {
+ return TruePredicate.TRUE;
+ }
+ AndPredicate result = new AndPredicate();
+ for (FilePredicate filePredicate : predicates) {
+ if (filePredicate == TruePredicate.TRUE) {
+ continue;
+ } else if (filePredicate == FalsePredicate.FALSE) {
+ return FalsePredicate.FALSE;
+ } else if (filePredicate instanceof AndPredicate) {
+ result.predicates.addAll(((AndPredicate) filePredicate).predicates);
+ } else {
+ result.predicates.add(OptimizedFilePredicateAdapter.create(filePredicate));
+ }
+ }
+ Collections.sort(result.predicates);
+ return result;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ for (OptimizedFilePredicate predicate : predicates) {
+ if (!predicate.apply(f)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Iterable<InputFile> filter(Iterable<InputFile> target) {
+ Iterable<InputFile> result = target;
+ for (OptimizedFilePredicate predicate : predicates) {
+ result = predicate.filter(result);
+ }
+ return result;
+ }
+
+ @Override
+ public Iterable<InputFile> get(Index index) {
+ if (predicates.isEmpty()) {
+ return index.inputFiles();
+ }
+ // Optimization, use get on first predicate then filter with next predicates
+ Iterable<InputFile> result = predicates.get(0).get(index);
+ for (int i = 1; i < predicates.size(); i++) {
+ result = predicates.get(i).filter(result);
+ }
+ return result;
+ }
+
+ Collection<OptimizedFilePredicate> predicates() {
+ return predicates;
+ }
+
+ @Override
+ public List<FilePredicate> operands() {
+ return predicates.stream().map(p -> (FilePredicate) p).collect(toList());
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicates.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicates.java
new file mode 100644
index 00000000000..fe005c608e7
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/DefaultFilePredicates.java
@@ -0,0 +1,214 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FilePredicates;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputFile.Status;
+import org.sonar.api.impl.fs.PathPattern;
+
+/**
+ * Factory of {@link FilePredicate}
+ *
+ * @since 4.2
+ */
+public class DefaultFilePredicates implements FilePredicates {
+
+ private final Path baseDir;
+
+ /**
+ * Client code should use {@link org.sonar.api.batch.fs.FileSystem#predicates()} to get an instance
+ */
+ public DefaultFilePredicates(Path baseDir) {
+ this.baseDir = baseDir;
+ }
+
+ /**
+ * Returns a predicate that always evaluates to true
+ */
+ @Override
+ public FilePredicate all() {
+ return TruePredicate.TRUE;
+ }
+
+ /**
+ * Returns a predicate that always evaluates to false
+ */
+ @Override
+ public FilePredicate none() {
+ return FalsePredicate.FALSE;
+ }
+
+ @Override
+ public FilePredicate hasAbsolutePath(String s) {
+ return new AbsolutePathPredicate(s, baseDir);
+ }
+
+ /**
+ * non-normalized path and Windows-style path are supported
+ */
+ @Override
+ public FilePredicate hasRelativePath(String s) {
+ return new RelativePathPredicate(s);
+ }
+
+ @Override
+ public FilePredicate hasFilename(String s) {
+ return new FilenamePredicate(s);
+ }
+
+ @Override
+ public FilePredicate hasExtension(String s) {
+ return new FileExtensionPredicate(s);
+ }
+
+ @Override
+ public FilePredicate hasURI(URI uri) {
+ return new URIPredicate(uri, baseDir);
+ }
+
+ @Override
+ public FilePredicate matchesPathPattern(String inclusionPattern) {
+ return new PathPatternPredicate(PathPattern.create(inclusionPattern));
+ }
+
+ @Override
+ public FilePredicate matchesPathPatterns(String[] inclusionPatterns) {
+ if (inclusionPatterns.length == 0) {
+ return TruePredicate.TRUE;
+ }
+ FilePredicate[] predicates = new FilePredicate[inclusionPatterns.length];
+ for (int i = 0; i < inclusionPatterns.length; i++) {
+ predicates[i] = new PathPatternPredicate(PathPattern.create(inclusionPatterns[i]));
+ }
+ return or(predicates);
+ }
+
+ @Override
+ public FilePredicate doesNotMatchPathPattern(String exclusionPattern) {
+ return not(matchesPathPattern(exclusionPattern));
+ }
+
+ @Override
+ public FilePredicate doesNotMatchPathPatterns(String[] exclusionPatterns) {
+ if (exclusionPatterns.length == 0) {
+ return TruePredicate.TRUE;
+ }
+ return not(matchesPathPatterns(exclusionPatterns));
+ }
+
+ @Override
+ public FilePredicate hasPath(String s) {
+ File file = new File(s);
+ if (file.isAbsolute()) {
+ return hasAbsolutePath(s);
+ }
+ return hasRelativePath(s);
+ }
+
+ @Override
+ public FilePredicate is(File ioFile) {
+ if (ioFile.isAbsolute()) {
+ return hasAbsolutePath(ioFile.getAbsolutePath());
+ }
+ return hasRelativePath(ioFile.getPath());
+ }
+
+ @Override
+ public FilePredicate hasLanguage(String language) {
+ return new LanguagePredicate(language);
+ }
+
+ @Override
+ public FilePredicate hasLanguages(Collection<String> languages) {
+ List<FilePredicate> list = new ArrayList<>();
+ for (String language : languages) {
+ list.add(hasLanguage(language));
+ }
+ return or(list);
+ }
+
+ @Override
+ public FilePredicate hasLanguages(String... languages) {
+ List<FilePredicate> list = new ArrayList<>();
+ for (String language : languages) {
+ list.add(hasLanguage(language));
+ }
+ return or(list);
+ }
+
+ @Override
+ public FilePredicate hasType(InputFile.Type type) {
+ return new TypePredicate(type);
+ }
+
+ @Override
+ public FilePredicate not(FilePredicate p) {
+ return new NotPredicate(p);
+ }
+
+ @Override
+ public FilePredicate or(Collection<FilePredicate> or) {
+ return OrPredicate.create(or);
+ }
+
+ @Override
+ public FilePredicate or(FilePredicate... or) {
+ return OrPredicate.create(Arrays.asList(or));
+ }
+
+ @Override
+ public FilePredicate or(FilePredicate first, FilePredicate second) {
+ return OrPredicate.create(Arrays.asList(first, second));
+ }
+
+ @Override
+ public FilePredicate and(Collection<FilePredicate> and) {
+ return AndPredicate.create(and);
+ }
+
+ @Override
+ public FilePredicate and(FilePredicate... and) {
+ return AndPredicate.create(Arrays.asList(and));
+ }
+
+ @Override
+ public FilePredicate and(FilePredicate first, FilePredicate second) {
+ return AndPredicate.create(Arrays.asList(first, second));
+ }
+
+ @Override
+ public FilePredicate hasStatus(Status status) {
+ return new StatusPredicate(status);
+ }
+
+ @Override
+ public FilePredicate hasAnyStatus() {
+ return new StatusPredicate(null);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FalsePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FalsePredicate.java
new file mode 100644
index 00000000000..d6c1d78a696
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FalsePredicate.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.Collections;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+
+class FalsePredicate extends AbstractFilePredicate {
+
+ static final FilePredicate FALSE = new FalsePredicate();
+
+ @Override
+ public boolean apply(InputFile inputFile) {
+ return false;
+ }
+
+ @Override
+ public Iterable<InputFile> filter(Iterable<InputFile> target) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Iterable<InputFile> get(Index index) {
+ return Collections.emptyList();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicate.java
new file mode 100644
index 00000000000..7775c567832
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FileExtensionPredicate.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.Locale;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * @since 6.3
+ */
+public class FileExtensionPredicate extends AbstractFilePredicate {
+
+ private final String extension;
+
+ public FileExtensionPredicate(String extension) {
+ this.extension = lowercase(extension);
+ }
+
+ @Override
+ public boolean apply(InputFile inputFile) {
+ return extension.equals(getExtension(inputFile));
+ }
+
+ @Override
+ public Iterable<InputFile> get(FileSystem.Index index) {
+ return index.getFilesByExtension(extension);
+ }
+
+ public static String getExtension(InputFile inputFile) {
+ return getExtension(inputFile.filename());
+ }
+
+ static String getExtension(String name) {
+ int index = name.lastIndexOf('.');
+ if (index < 0) {
+ return "";
+ }
+ return lowercase(name.substring(index + 1));
+ }
+
+ private static String lowercase(String extension) {
+ return extension.toLowerCase(Locale.ENGLISH);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FilenamePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FilenamePredicate.java
new file mode 100644
index 00000000000..94fac58852f
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/FilenamePredicate.java
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * @since 6.3
+ */
+public class FilenamePredicate extends AbstractFilePredicate {
+ private final String filename;
+
+ public FilenamePredicate(String filename) {
+ this.filename = filename;
+ }
+
+ @Override
+ public boolean apply(InputFile inputFile) {
+ return filename.equals(inputFile.filename());
+ }
+
+ @Override
+ public Iterable<InputFile> get(FileSystem.Index index) {
+ return index.getFilesByName(filename);
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/LanguagePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/LanguagePredicate.java
new file mode 100644
index 00000000000..e553625789a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/LanguagePredicate.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * @since 4.2
+ */
+class LanguagePredicate extends AbstractFilePredicate {
+ private final String language;
+
+ LanguagePredicate(String language) {
+ this.language = language;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return language.equals(f.language());
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/NotPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/NotPredicate.java
new file mode 100644
index 00000000000..4841c558826
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/NotPredicate.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.Arrays;
+import java.util.List;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * @since 4.2
+ */
+class NotPredicate extends AbstractFilePredicate implements OperatorPredicate {
+
+ private final FilePredicate predicate;
+
+ NotPredicate(FilePredicate predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return !predicate.apply(f);
+ }
+
+ @Override
+ public List<FilePredicate> operands() {
+ return Arrays.asList(predicate);
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OperatorPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OperatorPredicate.java
new file mode 100644
index 00000000000..bf8e566e144
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OperatorPredicate.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.List;
+import org.sonar.api.batch.fs.FilePredicate;
+
+/**
+ * A predicate that associate other predicates
+ */
+public interface OperatorPredicate extends FilePredicate {
+
+ List<FilePredicate> operands();
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicate.java
new file mode 100644
index 00000000000..aaba4c890d6
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicate.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * Optimized version of FilePredicate allowing to speed up query by looking at InputFile by index.
+ */
+public interface OptimizedFilePredicate extends FilePredicate, Comparable<OptimizedFilePredicate> {
+
+ /**
+ * Filter provided files to keep only the ones that are valid for this predicate
+ */
+ Iterable<InputFile> filter(Iterable<InputFile> inputFiles);
+
+ /**
+ * Get all files that are valid for this predicate.
+ */
+ Iterable<InputFile> get(FileSystem.Index index);
+
+ /**
+ * For optimization. FilePredicates will be applied in priority order. For example when doing
+ * p.and(p1, p2, p3) then p1, p2 and p3 will be applied according to their priority value. Higher priority value
+ * are applied first.
+ * Assign a high priority when the predicate will likely highly reduce the set of InputFiles to filter. Also
+ * {@link RelativePathPredicate} and AbsolutePathPredicate have a high priority since they are using cache index.
+ */
+ int priority();
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicateAdapter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicateAdapter.java
new file mode 100644
index 00000000000..5de4fee72b5
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OptimizedFilePredicateAdapter.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.InputFile;
+
+public class OptimizedFilePredicateAdapter extends AbstractFilePredicate {
+
+ private FilePredicate unoptimizedPredicate;
+
+ private OptimizedFilePredicateAdapter(FilePredicate unoptimizedPredicate) {
+ this.unoptimizedPredicate = unoptimizedPredicate;
+ }
+
+ @Override
+ public boolean apply(InputFile inputFile) {
+ return unoptimizedPredicate.apply(inputFile);
+ }
+
+ public static OptimizedFilePredicate create(FilePredicate predicate) {
+ if (predicate instanceof OptimizedFilePredicate) {
+ return (OptimizedFilePredicate) predicate;
+ } else {
+ return new OptimizedFilePredicateAdapter(predicate);
+ }
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OrPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OrPredicate.java
new file mode 100644
index 00000000000..3f93903c4da
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/OrPredicate.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * @since 4.2
+ */
+class OrPredicate extends AbstractFilePredicate implements OperatorPredicate {
+
+ private final List<FilePredicate> predicates = new ArrayList<>();
+
+ private OrPredicate() {
+ }
+
+ public static FilePredicate create(Collection<FilePredicate> predicates) {
+ if (predicates.isEmpty()) {
+ return TruePredicate.TRUE;
+ }
+ OrPredicate result = new OrPredicate();
+ for (FilePredicate filePredicate : predicates) {
+ if (filePredicate == TruePredicate.TRUE) {
+ return TruePredicate.TRUE;
+ } else if (filePredicate == FalsePredicate.FALSE) {
+ continue;
+ } else if (filePredicate instanceof OrPredicate) {
+ result.predicates.addAll(((OrPredicate) filePredicate).predicates);
+ } else {
+ result.predicates.add(filePredicate);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ for (FilePredicate predicate : predicates) {
+ if (predicate.apply(f)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Collection<FilePredicate> predicates() {
+ return predicates;
+ }
+
+ @Override
+ public List<FilePredicate> operands() {
+ return predicates;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/PathPatternPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/PathPatternPredicate.java
new file mode 100644
index 00000000000..1e276751134
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/PathPatternPredicate.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.nio.file.Paths;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.impl.fs.PathPattern;
+
+/**
+ * @since 4.2
+ */
+class PathPatternPredicate extends AbstractFilePredicate {
+
+ private final PathPattern pattern;
+
+ PathPatternPredicate(PathPattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return pattern.match(f.path(), Paths.get(f.relativePath()));
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/RelativePathPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/RelativePathPredicate.java
new file mode 100644
index 00000000000..a1cb9f42090
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/RelativePathPredicate.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.util.Collections;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.utils.PathUtils;
+
+/**
+ * @since 4.2
+ */
+public class RelativePathPredicate extends AbstractFilePredicate {
+
+ @Nullable
+ private final String path;
+
+ RelativePathPredicate(String path) {
+ this.path = PathUtils.sanitize(path);
+ }
+
+ public String path() {
+ return path;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ if (path == null) {
+ return false;
+ }
+
+ return path.equals(f.relativePath());
+ }
+
+ @Override
+ public Iterable<InputFile> get(Index index) {
+ if (path != null) {
+ InputFile f = index.inputFile(this.path);
+ if (f != null) {
+ return Collections.singletonList(f);
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public int priority() {
+ return USE_INDEX;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/StatusPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/StatusPredicate.java
new file mode 100644
index 00000000000..bcac4e04c49
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/StatusPredicate.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * @deprecated since 7.8
+ */
+@Deprecated
+public class StatusPredicate extends AbstractFilePredicate {
+
+ private final InputFile.Status status;
+
+ StatusPredicate(@Nullable InputFile.Status status) {
+ this.status = status;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return status == null || status == f.status();
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TruePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TruePredicate.java
new file mode 100644
index 00000000000..04d56ee387e
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TruePredicate.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+
+class TruePredicate extends AbstractFilePredicate {
+
+ static final FilePredicate TRUE = new TruePredicate();
+
+ @Override
+ public boolean apply(InputFile inputFile) {
+ return true;
+ }
+
+ @Override
+ public Iterable<InputFile> get(Index index) {
+ return index.inputFiles();
+ }
+
+ @Override
+ public Iterable<InputFile> filter(Iterable<InputFile> target) {
+ return target;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TypePredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TypePredicate.java
new file mode 100644
index 00000000000..f9d57058352
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/TypePredicate.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import org.sonar.api.batch.fs.InputFile;
+
+/**
+ * @since 4.2
+ */
+class TypePredicate extends AbstractFilePredicate {
+
+ private final InputFile.Type type;
+
+ TypePredicate(InputFile.Type type) {
+ this.type = type;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return type == f.type();
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/URIPredicate.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/URIPredicate.java
new file mode 100644
index 00000000000..60fea977b5d
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/URIPredicate.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.fs.predicates;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+import org.sonar.api.batch.fs.FileSystem.Index;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.scan.filesystem.PathResolver;
+
+/**
+ * @since 6.6
+ */
+class URIPredicate extends AbstractFilePredicate {
+
+ private final URI uri;
+ private final Path baseDir;
+
+ URIPredicate(URI uri, Path baseDir) {
+ this.baseDir = baseDir;
+ this.uri = uri;
+ }
+
+ @Override
+ public boolean apply(InputFile f) {
+ return uri.equals(f.uri());
+ }
+
+ @Override
+ public Iterable<InputFile> get(Index index) {
+ Path path = Paths.get(uri);
+ Optional<String> relative = PathResolver.relativize(baseDir, path);
+ if (!relative.isPresent()) {
+ return Collections.emptyList();
+ }
+ InputFile f = index.inputFile(relative.get());
+ return f != null ? Arrays.asList(f) : Collections.<InputFile>emptyList();
+ }
+
+ @Override
+ public int priority() {
+ return USE_INDEX;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/package-info.java
new file mode 100644
index 00000000000..aa33a1e9028
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/fs/predicates/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.fs.predicates;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/AbstractDefaultIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/AbstractDefaultIssue.java
new file mode 100644
index 00000000000..8705bf2fda0
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/AbstractDefaultIssue.java
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.issue;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.Issue.Flow;
+import org.sonar.api.batch.sensor.issue.IssueLocation;
+import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.impl.fs.DefaultInputDir;
+import org.sonar.api.impl.fs.DefaultInputModule;
+import org.sonar.api.impl.fs.DefaultInputProject;
+import org.sonar.api.impl.sensor.DefaultStorable;
+import org.sonar.api.utils.PathUtils;
+
+import static java.util.Collections.unmodifiableList;
+import static java.util.stream.Collectors.toList;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public abstract class AbstractDefaultIssue<T extends AbstractDefaultIssue> extends DefaultStorable {
+ protected IssueLocation primaryLocation;
+ protected List<List<IssueLocation>> flows = new ArrayList<>();
+ protected DefaultInputProject project;
+
+ protected AbstractDefaultIssue(DefaultInputProject project) {
+ this(project, null);
+ }
+
+ public AbstractDefaultIssue(DefaultInputProject project, @Nullable SensorStorage storage) {
+ super(storage);
+ this.project = project;
+ }
+
+ public IssueLocation primaryLocation() {
+ return primaryLocation;
+ }
+
+ public List<Flow> flows() {
+ return this.flows.stream()
+ .<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l)))
+ .collect(toList());
+ }
+
+ public NewIssueLocation newLocation() {
+ return new DefaultIssueLocation();
+ }
+
+ public T at(NewIssueLocation primaryLocation) {
+ checkArgument(primaryLocation != null, "Cannot use a location that is null");
+ checkState(this.primaryLocation == null, "at() already called");
+ this.primaryLocation = rewriteLocation((DefaultIssueLocation) primaryLocation);
+ checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component");
+ return (T) this;
+ }
+
+ public T addLocation(NewIssueLocation secondaryLocation) {
+ flows.add(Collections.singletonList(rewriteLocation((DefaultIssueLocation) secondaryLocation)));
+ return (T) this;
+ }
+
+ public T addFlow(Iterable<NewIssueLocation> locations) {
+ List<IssueLocation> flowAsList = new ArrayList<>();
+ for (NewIssueLocation issueLocation : locations) {
+ flowAsList.add(rewriteLocation((DefaultIssueLocation) issueLocation));
+ }
+ flows.add(flowAsList);
+ return (T) this;
+ }
+
+ private DefaultIssueLocation rewriteLocation(DefaultIssueLocation location) {
+ InputComponent component = location.inputComponent();
+ Optional<Path> dirOrModulePath = Optional.empty();
+
+ if (component instanceof DefaultInputDir) {
+ DefaultInputDir dirComponent = (DefaultInputDir) component;
+ dirOrModulePath = Optional.of(project.getBaseDir().relativize(dirComponent.path()));
+ } else if (component instanceof DefaultInputModule && !Objects.equals(project.key(), component.key())) {
+ DefaultInputModule moduleComponent = (DefaultInputModule) component;
+ dirOrModulePath = Optional.of(project.getBaseDir().relativize(moduleComponent.getBaseDir()));
+ }
+
+ if (dirOrModulePath.isPresent()) {
+ String path = PathUtils.sanitize(dirOrModulePath.get().toString());
+ DefaultIssueLocation fixedLocation = new DefaultIssueLocation();
+ fixedLocation.on(project);
+ StringBuilder fullMessage = new StringBuilder();
+ if (path != null && !path.isEmpty()) {
+ fullMessage.append("[").append(path).append("] ");
+ }
+ fullMessage.append(location.message());
+ fixedLocation.message(fullMessage.toString());
+ return fixedLocation;
+ } else {
+ return location;
+ }
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssue.java
new file mode 100644
index 00000000000..aebe8f90a61
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssue.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.issue;
+
+import javax.annotation.Nullable;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.issue.IssueLocation;
+import org.sonar.api.batch.sensor.issue.NewIssue;
+import org.sonar.api.impl.fs.DefaultInputProject;
+import org.sonar.api.rule.RuleKey;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultIssue extends AbstractDefaultIssue<DefaultIssue> implements Issue, NewIssue {
+ private RuleKey ruleKey;
+ private Double gap;
+ private Severity overriddenSeverity;
+
+ public DefaultIssue(DefaultInputProject project) {
+ this(project, null);
+ }
+
+ public DefaultIssue(DefaultInputProject project, @Nullable SensorStorage storage) {
+ super(project, storage);
+ }
+
+ public DefaultIssue forRule(RuleKey ruleKey) {
+ this.ruleKey = ruleKey;
+ return this;
+ }
+
+ public RuleKey ruleKey() {
+ return this.ruleKey;
+ }
+
+ @Override
+ public DefaultIssue gap(@Nullable Double gap) {
+ checkArgument(gap == null || gap >= 0, format("Gap must be greater than or equal 0 (got %s)", gap));
+ this.gap = gap;
+ return this;
+ }
+
+ @Override
+ public DefaultIssue overrideSeverity(@Nullable Severity severity) {
+ this.overriddenSeverity = severity;
+ return this;
+ }
+
+ @Override
+ public Severity overriddenSeverity() {
+ return this.overriddenSeverity;
+ }
+
+ @Override
+ public Double gap() {
+ return this.gap;
+ }
+
+ @Override
+ public IssueLocation primaryLocation() {
+ return primaryLocation;
+ }
+
+ @Override
+ public void doSave() {
+ requireNonNull(this.ruleKey, "ruleKey is mandatory on issue");
+ checkState(primaryLocation != null, "Primary location is mandatory on every issue");
+ storage.store(this);
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssueLocation.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssueLocation.java
new file mode 100644
index 00000000000..6b2329c81c2
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultIssueLocation.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.issue;
+
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.sensor.issue.IssueLocation;
+import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.impl.fs.DefaultInputFile;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.commons.lang.StringUtils.abbreviate;
+import static org.apache.commons.lang.StringUtils.trim;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultIssueLocation implements NewIssueLocation, IssueLocation {
+
+ private InputComponent component;
+ private TextRange textRange;
+ private String message;
+
+ @Override
+ public DefaultIssueLocation on(InputComponent component) {
+ checkArgument(component != null, "Component can't be null");
+ checkState(this.component == null, "on() already called");
+ this.component = component;
+ return this;
+ }
+
+ @Override
+ public DefaultIssueLocation at(TextRange location) {
+ checkState(this.component != null, "at() should be called after on()");
+ checkState(this.component.isFile(), "at() should be called only for an InputFile.");
+ DefaultInputFile file = (DefaultInputFile) this.component;
+ file.validate(location);
+ this.textRange = location;
+ return this;
+ }
+
+ @Override
+ public DefaultIssueLocation message(String message) {
+ requireNonNull(message, "Message can't be null");
+ if (message.contains("\u0000")) {
+ throw new IllegalArgumentException(unsupportedCharacterError(message, component));
+ }
+ this.message = abbreviate(trim(message), MESSAGE_MAX_SIZE);
+ return this;
+ }
+
+ private static String unsupportedCharacterError(String message, @Nullable InputComponent component) {
+ String error = "Character \\u0000 is not supported in issue message '" + message + "'";
+ if (component != null) {
+ error += ", on component: " + component.toString();
+ }
+ return error;
+ }
+
+ @Override
+ public InputComponent inputComponent() {
+ return this.component;
+ }
+
+ @Override
+ public TextRange textRange() {
+ return textRange;
+ }
+
+ @Override
+ public String message() {
+ return this.message;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultNoSonarFilter.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultNoSonarFilter.java
new file mode 100644
index 00000000000..3bef1e9e5ef
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/DefaultNoSonarFilter.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.issue;
+
+import java.util.Set;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.issue.NoSonarFilter;
+import org.sonar.api.impl.fs.DefaultInputFile;
+
+public class DefaultNoSonarFilter extends NoSonarFilter {
+ public NoSonarFilter noSonarInFile(InputFile inputFile, Set<Integer> noSonarLines) {
+ ((DefaultInputFile) inputFile).noSonarAt(noSonarLines);
+ return this;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/package-info.java
new file mode 100644
index 00000000000..8d4146ce55a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/issue/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.issue;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/ActiveRulesBuilder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/ActiveRulesBuilder.java
new file mode 100644
index 00000000000..318a4738291
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/ActiveRulesBuilder.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.rule;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.NewActiveRule;
+import org.sonar.api.rule.RuleKey;
+
+/**
+ * Builds instances of {@link org.sonar.api.batch.rule.ActiveRules}.
+ * <b>For unit testing and internal use only</b>.
+ *
+ * @since 4.2
+ */
+public class ActiveRulesBuilder {
+
+ private final Map<RuleKey, NewActiveRule> map = new LinkedHashMap<>();
+
+ public ActiveRulesBuilder addRule(NewActiveRule newActiveRule) {
+ if (map.containsKey(newActiveRule.ruleKey())) {
+ throw new IllegalStateException(String.format("Rule '%s' is already activated", newActiveRule.ruleKey()));
+ }
+ map.put(newActiveRule.ruleKey(), newActiveRule);
+ return this;
+ }
+
+ public ActiveRules build() {
+ return new DefaultActiveRules(map.values());
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultActiveRules.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultActiveRules.java
new file mode 100644
index 00000000000..9add14cc484
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultActiveRules.java
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.rule;
+
+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 java.util.stream.Collectors;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.batch.rule.ActiveRule;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.DefaultActiveRule;
+import org.sonar.api.batch.rule.NewActiveRule;
+import org.sonar.api.rule.RuleKey;
+
+@Immutable
+public class DefaultActiveRules implements ActiveRules {
+ private final Map<String, List<ActiveRule>> activeRulesByRepository = new HashMap<>();
+ private final Map<String, Map<String, ActiveRule>> activeRulesByRepositoryAndKey = new HashMap<>();
+ private final Map<String, Map<String, ActiveRule>> activeRulesByRepositoryAndInternalKey = new HashMap<>();
+ private final Map<String, List<ActiveRule>> activeRulesByLanguage = new HashMap<>();
+
+ public DefaultActiveRules(Collection<NewActiveRule> newActiveRules) {
+ for (NewActiveRule newAR : newActiveRules) {
+ DefaultActiveRule ar = new DefaultActiveRule(newAR);
+ String repo = ar.ruleKey().repository();
+ activeRulesByRepository.computeIfAbsent(repo, x -> new ArrayList<>()).add(ar);
+ if (ar.language() != null) {
+ activeRulesByLanguage.computeIfAbsent(ar.language(), x -> new ArrayList<>()).add(ar);
+ }
+
+ activeRulesByRepositoryAndKey.computeIfAbsent(repo, r -> new HashMap<>()).put(ar.ruleKey().rule(), ar);
+ String internalKey = ar.internalKey();
+ if (internalKey != null) {
+ activeRulesByRepositoryAndInternalKey.computeIfAbsent(repo, r -> new HashMap<>()).put(internalKey, ar);
+ }
+ }
+ }
+
+ @Override
+ public ActiveRule find(RuleKey ruleKey) {
+ return activeRulesByRepositoryAndKey.getOrDefault(ruleKey.repository(), Collections.emptyMap())
+ .get(ruleKey.rule());
+ }
+
+ @Override
+ public Collection<ActiveRule> findAll() {
+ return activeRulesByRepository.entrySet().stream().flatMap(x -> x.getValue().stream()).collect(Collectors.toList());
+ }
+
+ @Override
+ public Collection<ActiveRule> findByRepository(String repository) {
+ return activeRulesByRepository.getOrDefault(repository, Collections.emptyList());
+ }
+
+ @Override
+ public Collection<ActiveRule> findByLanguage(String language) {
+ return activeRulesByLanguage.getOrDefault(language, Collections.emptyList());
+ }
+
+ @Override
+ public ActiveRule findByInternalKey(String repository, String internalKey) {
+ return activeRulesByRepositoryAndInternalKey.containsKey(repository) ? activeRulesByRepositoryAndInternalKey.get(repository).get(internalKey) : null;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultRules.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultRules.java
new file mode 100644
index 00000000000..c7c7b5ead44
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/DefaultRules.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.rule;
+
+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 java.util.stream.Collectors;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.batch.rule.DefaultRule;
+import org.sonar.api.batch.rule.NewRule;
+import org.sonar.api.batch.rule.Rule;
+import org.sonar.api.batch.rule.Rules;
+import org.sonar.api.rule.RuleKey;
+
+@Immutable
+class DefaultRules implements Rules {
+ private final Map<String, List<Rule>> rulesByRepository;
+ private final Map<String, Map<String, List<Rule>>> rulesByRepositoryAndInternalKey;
+ private final Map<RuleKey, Rule> rulesByRuleKey;
+
+ DefaultRules(Collection<NewRule> newRules) {
+ Map<String, List<Rule>> rulesByRepositoryBuilder = new HashMap<>();
+ Map<String, Map<String, List<Rule>>> rulesByRepositoryAndInternalKeyBuilder = new HashMap<>();
+ Map<RuleKey, Rule> rulesByRuleKeyBuilder = new HashMap<>();
+
+ for (NewRule newRule : newRules) {
+ DefaultRule r = new DefaultRule(newRule);
+ rulesByRuleKeyBuilder.put(r.key(), r);
+ rulesByRepositoryBuilder.computeIfAbsent(r.key().repository(), x -> new ArrayList<>()).add(r);
+ addToTable(rulesByRepositoryAndInternalKeyBuilder, r);
+ }
+
+ rulesByRuleKey = Collections.unmodifiableMap(rulesByRuleKeyBuilder);
+ rulesByRepository = Collections.unmodifiableMap(rulesByRepositoryBuilder);
+ rulesByRepositoryAndInternalKey = Collections.unmodifiableMap(rulesByRepositoryAndInternalKeyBuilder);
+ }
+
+ private static void addToTable(Map<String, Map<String, List<Rule>>> rulesByRepositoryAndInternalKeyBuilder, DefaultRule r) {
+ if (r.internalKey() == null) {
+ return;
+ }
+
+ rulesByRepositoryAndInternalKeyBuilder
+ .computeIfAbsent(r.key().repository(), x -> new HashMap<>())
+ .computeIfAbsent(r.internalKey(), x -> new ArrayList<>())
+ .add(r);
+ }
+
+ @Override
+ public Rule find(RuleKey ruleKey) {
+ return rulesByRuleKey.get(ruleKey);
+ }
+
+ @Override
+ public Collection<Rule> findAll() {
+ return rulesByRepository.values().stream().flatMap(List::stream).collect(Collectors.toList());
+ }
+
+ @Override
+ public Collection<Rule> findByRepository(String repository) {
+ return rulesByRepository.getOrDefault(repository, Collections.emptyList());
+ }
+
+ @Override
+ public Collection<Rule> findByInternalKey(String repository, String internalKey) {
+ return rulesByRepositoryAndInternalKey
+ .getOrDefault(repository, Collections.emptyMap())
+ .getOrDefault(internalKey, Collections.emptyList());
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/RulesBuilder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/RulesBuilder.java
new file mode 100644
index 00000000000..36c84bef34a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/rule/RulesBuilder.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.rule;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.sonar.api.batch.rule.NewRule;
+import org.sonar.api.batch.rule.Rules;
+import org.sonar.api.rule.RuleKey;
+
+/**
+ * For unit testing and internal use only.
+ *
+ * @since 4.2
+ */
+
+public class RulesBuilder {
+
+ private final Map<RuleKey, NewRule> map = new HashMap<>();
+
+ public NewRule add(RuleKey key) {
+ if (map.containsKey(key)) {
+ throw new IllegalStateException(String.format("Rule '%s' already exists", key));
+ }
+ NewRule newRule = new NewRule(key);
+ map.put(key, newRule);
+ return newRule;
+ }
+
+ public Rules build() {
+ return new DefaultRules(map.values());
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAdHocRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAdHocRule.java
new file mode 100644
index 00000000000..1513fa2f695
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAdHocRule.java
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.rule.AdHocRule;
+import org.sonar.api.batch.sensor.rule.NewAdHocRule;
+import org.sonar.api.rules.RuleType;
+
+import static org.apache.commons.lang.StringUtils.isNotBlank;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultAdHocRule extends DefaultStorable implements AdHocRule, NewAdHocRule {
+ private Severity severity;
+ private RuleType type;
+ private String name;
+ private String description;
+ private String engineId;
+ private String ruleId;
+
+ public DefaultAdHocRule() {
+ super(null);
+ }
+
+ public DefaultAdHocRule(@Nullable SensorStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public DefaultAdHocRule severity(Severity severity) {
+ this.severity = severity;
+ return this;
+ }
+
+ @Override
+ public String engineId() {
+ return engineId;
+ }
+
+ @Override
+ public String ruleId() {
+ return ruleId;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @CheckForNull
+ @Override
+ public String description() {
+ return description;
+ }
+
+ @Override
+ public Severity severity() {
+ return this.severity;
+ }
+
+ @Override
+ public void doSave() {
+ checkState(isNotBlank(engineId), "Engine id is mandatory on ad hoc rule");
+ checkState(isNotBlank(ruleId), "Rule id is mandatory on ad hoc rule");
+ checkState(isNotBlank(name), "Name is mandatory on every ad hoc rule");
+ checkState(severity != null, "Severity is mandatory on every ad hoc rule");
+ checkState(type != null, "Type is mandatory on every ad hoc rule");
+ storage.store(this);
+ }
+
+ @Override
+ public RuleType type() {
+ return type;
+ }
+
+ @Override
+ public DefaultAdHocRule engineId(String engineId) {
+ this.engineId = engineId;
+ return this;
+ }
+
+ @Override
+ public DefaultAdHocRule ruleId(String ruleId) {
+ this.ruleId = ruleId;
+ return this;
+ }
+
+ @Override
+ public DefaultAdHocRule name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public DefaultAdHocRule description(@Nullable String description) {
+ this.description = description;
+ return this;
+ }
+
+ @Override
+ public DefaultAdHocRule type(RuleType type) {
+ this.type = type;
+ return this;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAnalysisError.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAnalysisError.java
new file mode 100644
index 00000000000..5508461ce68
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultAnalysisError.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextPointer;
+import org.sonar.api.batch.sensor.error.AnalysisError;
+import org.sonar.api.batch.sensor.error.NewAnalysisError;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultAnalysisError extends DefaultStorable implements NewAnalysisError, AnalysisError {
+ private InputFile inputFile;
+ private String message;
+ private TextPointer location;
+
+ public DefaultAnalysisError() {
+ super(null);
+ }
+
+ public DefaultAnalysisError(SensorStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public InputFile inputFile() {
+ return inputFile;
+ }
+
+ @Override
+ public String message() {
+ return message;
+ }
+
+ @Override
+ public TextPointer location() {
+ return location;
+ }
+
+ @Override
+ public NewAnalysisError onFile(InputFile inputFile) {
+ checkArgument(inputFile != null, "Cannot use a inputFile that is null");
+ checkState(this.inputFile == null, "onFile() already called");
+ this.inputFile = inputFile;
+ return this;
+ }
+
+ @Override
+ public NewAnalysisError message(String message) {
+ this.message = message;
+ return this;
+ }
+
+ @Override
+ public NewAnalysisError at(TextPointer location) {
+ checkState(this.location == null, "at() already called");
+ this.location = location;
+ return this;
+ }
+
+ @Override
+ protected void doSave() {
+ requireNonNull(this.inputFile, "inputFile is mandatory on AnalysisError");
+ storage.store(this);
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCoverage.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCoverage.java
new file mode 100644
index 00000000000..c4149ec2f63
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCoverage.java
@@ -0,0 +1,157 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.util.Collections;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.sensor.coverage.CoverageType;
+import org.sonar.api.batch.sensor.coverage.NewCoverage;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.impl.fs.DefaultInputFile;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultCoverage extends DefaultStorable implements NewCoverage {
+
+ private InputFile inputFile;
+ private CoverageType type;
+ private int totalCoveredLines = 0;
+ private int totalConditions = 0;
+ private int totalCoveredConditions = 0;
+ private SortedMap<Integer, Integer> hitsByLine = new TreeMap<>();
+ private SortedMap<Integer, Integer> conditionsByLine = new TreeMap<>();
+ private SortedMap<Integer, Integer> coveredConditionsByLine = new TreeMap<>();
+
+ public DefaultCoverage() {
+ super();
+ }
+
+ public DefaultCoverage(@Nullable SensorStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public DefaultCoverage onFile(InputFile inputFile) {
+ this.inputFile = inputFile;
+ return this;
+ }
+
+ public InputFile inputFile() {
+ return inputFile;
+ }
+
+ @Override
+ public NewCoverage ofType(CoverageType type) {
+ this.type = requireNonNull(type, "type can't be null");
+ return this;
+ }
+
+ public CoverageType type() {
+ return type;
+ }
+
+ @Override
+ public NewCoverage lineHits(int line, int hits) {
+ validateFile();
+ if (isExcluded()) {
+ return this;
+ }
+ validateLine(line);
+
+ if (!hitsByLine.containsKey(line)) {
+ hitsByLine.put(line, hits);
+ if (hits > 0) {
+ totalCoveredLines += 1;
+ }
+ }
+ return this;
+ }
+
+ private void validateLine(int line) {
+ checkState(line <= inputFile.lines(), "Line %s is out of range in the file %s (lines: %s)", line, inputFile, inputFile.lines());
+ checkState(line > 0, "Line number must be strictly positive: %s", line);
+ }
+
+ private void validateFile() {
+ requireNonNull(inputFile, "Call onFile() first");
+ }
+
+ @Override
+ public NewCoverage conditions(int line, int conditions, int coveredConditions) {
+ validateFile();
+ if (isExcluded()) {
+ return this;
+ }
+ validateLine(line);
+
+ if (conditions > 0 && !conditionsByLine.containsKey(line)) {
+ totalConditions += conditions;
+ totalCoveredConditions += coveredConditions;
+ conditionsByLine.put(line, conditions);
+ coveredConditionsByLine.put(line, coveredConditions);
+ }
+ return this;
+ }
+
+ public int coveredLines() {
+ return totalCoveredLines;
+ }
+
+ public int linesToCover() {
+ return hitsByLine.size();
+ }
+
+ public int conditions() {
+ return totalConditions;
+ }
+
+ public int coveredConditions() {
+ return totalCoveredConditions;
+ }
+
+ public SortedMap<Integer, Integer> hitsByLine() {
+ return Collections.unmodifiableSortedMap(hitsByLine);
+ }
+
+ public SortedMap<Integer, Integer> conditionsByLine() {
+ return Collections.unmodifiableSortedMap(conditionsByLine);
+ }
+
+ public SortedMap<Integer, Integer> coveredConditionsByLine() {
+ return Collections.unmodifiableSortedMap(coveredConditionsByLine);
+ }
+
+ @Override
+ public void doSave() {
+ validateFile();
+ if (!isExcluded()) {
+ storage.store(this);
+ }
+ }
+
+ private boolean isExcluded() {
+ return ((DefaultInputFile) inputFile).isExcludedForCoverage();
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCpdTokens.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCpdTokens.java
new file mode 100644
index 00000000000..9b789d22d14
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultCpdTokens.java
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
+import org.sonar.api.batch.sensor.cpd.internal.TokensLine;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.impl.fs.DefaultInputFile;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.util.Collections.unmodifiableList;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultCpdTokens extends DefaultStorable implements NewCpdTokens {
+ private static final Logger LOG = Loggers.get(DefaultCpdTokens.class);
+ private final List<TokensLine> result = new ArrayList<>();
+ private DefaultInputFile inputFile;
+ private int startLine = Integer.MIN_VALUE;
+ private int startIndex = 0;
+ private int currentIndex = 0;
+ private StringBuilder sb = new StringBuilder();
+ private TextRange lastRange;
+ private boolean loggedTestCpdWarning = false;
+
+ public DefaultCpdTokens(SensorStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public DefaultCpdTokens onFile(InputFile inputFile) {
+ this.inputFile = (DefaultInputFile) requireNonNull(inputFile, "file can't be null");
+ return this;
+ }
+
+ public InputFile inputFile() {
+ return inputFile;
+ }
+
+ @Override
+ public NewCpdTokens addToken(int startLine, int startLineOffset, int endLine, int endLineOffset, String image) {
+ checkInputFileNotNull();
+ TextRange newRange;
+ try {
+ newRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to register token in file " + inputFile, e);
+ }
+ return addToken(newRange, image);
+ }
+
+ @Override
+ public DefaultCpdTokens addToken(TextRange range, String image) {
+ requireNonNull(range, "Range should not be null");
+ requireNonNull(image, "Image should not be null");
+ checkInputFileNotNull();
+ if (isExcludedForDuplication()) {
+ return this;
+ }
+ checkState(lastRange == null || lastRange.end().compareTo(range.start()) <= 0,
+ "Tokens of file %s should be provided in order.\nPrevious token: %s\nLast token: %s", inputFile, lastRange, range);
+
+ int line = range.start().line();
+ if (line != startLine) {
+ addNewTokensLine(result, startIndex, currentIndex, startLine, sb);
+ startIndex = currentIndex + 1;
+ startLine = line;
+ }
+ currentIndex++;
+ sb.append(image);
+ lastRange = range;
+
+ return this;
+ }
+
+ private boolean isExcludedForDuplication() {
+ if (inputFile.isExcludedForDuplication()) {
+ return true;
+ }
+ if (inputFile.type() == InputFile.Type.TEST) {
+ if (!loggedTestCpdWarning) {
+ LOG.warn("Duplication reported for '{}' will be ignored because it's a test file.", inputFile);
+ loggedTestCpdWarning = true;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public List<TokensLine> getTokenLines() {
+ return unmodifiableList(new ArrayList<>(result));
+ }
+
+ private static void addNewTokensLine(List<TokensLine> result, int startUnit, int endUnit, int startLine, StringBuilder sb) {
+ if (sb.length() != 0) {
+ result.add(new TokensLine(startUnit, endUnit, startLine, sb.toString()));
+ sb.setLength(0);
+ }
+ }
+
+ @Override
+ protected void doSave() {
+ checkState(inputFile != null, "Call onFile() first");
+ if (isExcludedForDuplication()) {
+ return;
+ }
+ addNewTokensLine(result, startIndex, currentIndex, startLine, sb);
+ storage.store(this);
+ }
+
+ private void checkInputFileNotNull() {
+ checkState(inputFile != null, "Call onFile() first");
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultExternalIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultExternalIssue.java
new file mode 100644
index 00000000000..ae17adbaca6
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultExternalIssue.java
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import javax.annotation.Nullable;
+import org.sonar.api.batch.rule.Severity;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
+import org.sonar.api.impl.fs.DefaultInputProject;
+import org.sonar.api.impl.issue.AbstractDefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultExternalIssue extends AbstractDefaultIssue<DefaultExternalIssue> implements ExternalIssue, NewExternalIssue {
+ private Long effort;
+ private Severity severity;
+ private RuleType type;
+ private String engineId;
+ private String ruleId;
+
+ public DefaultExternalIssue(DefaultInputProject project) {
+ this(project, null);
+ }
+
+ public DefaultExternalIssue(DefaultInputProject project, @Nullable SensorStorage storage) {
+ super(project, storage);
+ }
+
+ @Override
+ public DefaultExternalIssue remediationEffortMinutes(@Nullable Long effort) {
+ checkArgument(effort == null || effort >= 0, format("effort must be greater than or equal 0 (got %s)", effort));
+ this.effort = effort;
+ return this;
+ }
+
+ @Override
+ public DefaultExternalIssue severity(Severity severity) {
+ this.severity = severity;
+ return this;
+ }
+
+ @Override
+ public String engineId() {
+ return engineId;
+ }
+
+ @Override
+ public String ruleId() {
+ return ruleId;
+ }
+
+ @Override
+ public Severity severity() {
+ return this.severity;
+ }
+
+ @Override
+ public Long remediationEffort() {
+ return this.effort;
+ }
+
+ @Override
+ public void doSave() {
+ requireNonNull(this.engineId, "Engine id is mandatory on external issue");
+ requireNonNull(this.ruleId, "Rule id is mandatory on external issue");
+ checkState(primaryLocation != null, "Primary location is mandatory on every external issue");
+ checkState(primaryLocation.inputComponent().isFile(), "External issues must be located in files");
+ checkState(primaryLocation.message() != null, "External issues must have a message");
+ checkState(severity != null, "Severity is mandatory on every external issue");
+ checkState(type != null, "Type is mandatory on every external issue");
+ storage.store(this);
+ }
+
+ @Override
+ public RuleType type() {
+ return type;
+ }
+
+ @Override
+ public NewExternalIssue engineId(String engineId) {
+ this.engineId = engineId;
+ return this;
+ }
+
+ @Override
+ public NewExternalIssue ruleId(String ruleId) {
+ this.ruleId = ruleId;
+ return this;
+ }
+
+ @Override
+ public DefaultExternalIssue forRule(RuleKey ruleKey) {
+ this.engineId = ruleKey.repository();
+ this.ruleId = ruleKey.rule();
+ return this;
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ if (engineId != null && ruleId != null) {
+ return RuleKey.of(RuleKey.EXTERNAL_RULE_REPO_PREFIX + engineId, ruleId);
+ }
+ return null;
+ }
+
+ @Override
+ public DefaultExternalIssue type(RuleType type) {
+ this.type = type;
+ return this;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultHighlighting.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultHighlighting.java
new file mode 100644
index 00000000000..c7e133dfea7
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultHighlighting.java
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.impl.fs.DefaultInputFile;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultHighlighting extends DefaultStorable implements NewHighlighting {
+
+ private final List<SyntaxHighlightingRule> syntaxHighlightingRules;
+ private DefaultInputFile inputFile;
+
+ public DefaultHighlighting(SensorStorage storage) {
+ super(storage);
+ syntaxHighlightingRules = new ArrayList<>();
+ }
+
+ public List<SyntaxHighlightingRule> getSyntaxHighlightingRuleSet() {
+ return syntaxHighlightingRules;
+ }
+
+ private void checkOverlappingBoundaries() {
+ if (syntaxHighlightingRules.size() > 1) {
+ Iterator<SyntaxHighlightingRule> it = syntaxHighlightingRules.iterator();
+ SyntaxHighlightingRule previous = it.next();
+ while (it.hasNext()) {
+ SyntaxHighlightingRule current = it.next();
+ if (previous.range().end().compareTo(current.range().start()) > 0 && (previous.range().end().compareTo(current.range().end()) < 0)) {
+ String errorMsg = String.format("Cannot register highlighting rule for characters at %s as it " +
+ "overlaps at least one existing rule", current.range());
+ throw new IllegalStateException(errorMsg);
+ }
+ previous = current;
+ }
+ }
+ }
+
+ @Override
+ public DefaultHighlighting onFile(InputFile inputFile) {
+ requireNonNull(inputFile, "file can't be null");
+ this.inputFile = (DefaultInputFile) inputFile;
+ return this;
+ }
+
+ public InputFile inputFile() {
+ return inputFile;
+ }
+
+ @Override
+ public DefaultHighlighting highlight(int startOffset, int endOffset, TypeOfText typeOfText) {
+ checkInputFileNotNull();
+ TextRange newRange;
+ try {
+ newRange = inputFile.newRange(startOffset, endOffset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to highlight file " + inputFile, e);
+ }
+ return highlight(newRange, typeOfText);
+ }
+
+ @Override
+ public DefaultHighlighting highlight(int startLine, int startLineOffset, int endLine, int endLineOffset, TypeOfText typeOfText) {
+ checkInputFileNotNull();
+ TextRange newRange;
+ try {
+ newRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to highlight file " + inputFile, e);
+ }
+ return highlight(newRange, typeOfText);
+ }
+
+ @Override
+ public DefaultHighlighting highlight(TextRange range, TypeOfText typeOfText) {
+ SyntaxHighlightingRule syntaxHighlightingRule = SyntaxHighlightingRule.create(range, typeOfText);
+ this.syntaxHighlightingRules.add(syntaxHighlightingRule);
+ return this;
+ }
+
+ @Override
+ protected void doSave() {
+ checkInputFileNotNull();
+ // Sort rules to avoid variation during consecutive runs
+ Collections.sort(syntaxHighlightingRules, (left, right) -> {
+ int result = left.range().start().compareTo(right.range().start());
+ if (result == 0) {
+ result = right.range().end().compareTo(left.range().end());
+ }
+ return result;
+ });
+ checkOverlappingBoundaries();
+ storage.store(this);
+ }
+
+ private void checkInputFileNotNull() {
+ checkState(inputFile != null, "Call onFile() first");
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultMeasure.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultMeasure.java
new file mode 100644
index 00000000000..d2919aba182
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultMeasure.java
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.io.Serializable;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.sonar.api.batch.fs.InputComponent;
+import org.sonar.api.batch.measure.Metric;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.measure.Measure;
+import org.sonar.api.batch.sensor.measure.NewMeasure;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultMeasure<G extends Serializable> extends DefaultStorable implements Measure<G>, NewMeasure<G> {
+
+ private InputComponent component;
+ private Metric<G> metric;
+ private G value;
+ private boolean fromCore = false;
+
+ public DefaultMeasure() {
+ super();
+ }
+
+ public DefaultMeasure(@Nullable SensorStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public DefaultMeasure<G> on(InputComponent component) {
+ checkArgument(component != null, "Component can't be null");
+ checkState(this.component == null, "on() already called");
+ this.component = component;
+ return this;
+ }
+
+ @Override
+ public DefaultMeasure<G> forMetric(Metric<G> metric) {
+ checkState(this.metric == null, "Metric already defined");
+ requireNonNull(metric, "metric should be non null");
+ this.metric = metric;
+ return this;
+ }
+
+ @Override
+ public DefaultMeasure<G> withValue(G value) {
+ checkState(this.value == null, "Measure value already defined");
+ requireNonNull(value, "Measure value can't be null");
+ this.value = value;
+ return this;
+ }
+
+ /**
+ * For internal use.
+ */
+ public boolean isFromCore() {
+ return fromCore;
+ }
+
+ /**
+ * For internal use. Used by core components to bypass check that prevent a plugin to store core measures.
+ */
+ public DefaultMeasure<G> setFromCore() {
+ this.fromCore = true;
+ return this;
+ }
+
+ @Override
+ public void doSave() {
+ requireNonNull(this.value, "Measure value can't be null");
+ requireNonNull(this.metric, "Measure metric can't be null");
+ checkState(this.metric.valueType().equals(this.value.getClass()), "Measure value should be of type %s", this.metric.valueType());
+ storage.store(this);
+ }
+
+ @Override
+ public Metric<G> metric() {
+ return metric;
+ }
+
+ @Override
+ public InputComponent inputComponent() {
+ return component;
+ }
+
+ @Override
+ public G value() {
+ return value;
+ }
+
+ // For testing purpose
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ if (obj.getClass() != getClass()) {
+ return false;
+ }
+ DefaultMeasure<?> rhs = (DefaultMeasure<?>) obj;
+ return new EqualsBuilder()
+ .append(component, rhs.component)
+ .append(metric, rhs.metric)
+ .append(value, rhs.value)
+ .isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(27, 45).append(component).append(metric).append(value).toHashCode();
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultPostJobDescriptor.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultPostJobDescriptor.java
new file mode 100644
index 00000000000..152c5719e0a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultPostJobDescriptor.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.util.Arrays;
+import java.util.Collection;
+import org.sonar.api.batch.postjob.PostJobDescriptor;
+
+public class DefaultPostJobDescriptor implements PostJobDescriptor {
+
+ private String name;
+ private String[] properties = new String[0];
+
+ public String name() {
+ return name;
+ }
+
+ public Collection<String> properties() {
+ return Arrays.asList(properties);
+ }
+
+ @Override
+ public DefaultPostJobDescriptor name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public DefaultPostJobDescriptor requireProperty(String... propertyKey) {
+ return requireProperties(propertyKey);
+ }
+
+ @Override
+ public DefaultPostJobDescriptor requireProperties(String... propertyKeys) {
+ this.properties = propertyKeys;
+ return this;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSensorDescriptor.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSensorDescriptor.java
new file mode 100644
index 00000000000..5b60ac287e2
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSensorDescriptor.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.function.Predicate;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.config.Configuration;
+
+import static java.util.Arrays.asList;
+
+public class DefaultSensorDescriptor implements SensorDescriptor {
+
+ private String name;
+ private String[] languages = new String[0];
+ private InputFile.Type type = null;
+ private String[] ruleRepositories = new String[0];
+ private boolean global = false;
+ private Predicate<Configuration> configurationPredicate;
+
+ public String name() {
+ return name;
+ }
+
+ public Collection<String> languages() {
+ return Arrays.asList(languages);
+ }
+
+ @Nullable
+ public InputFile.Type type() {
+ return type;
+ }
+
+ public Collection<String> ruleRepositories() {
+ return Arrays.asList(ruleRepositories);
+ }
+
+ public Predicate<Configuration> configurationPredicate() {
+ return configurationPredicate;
+ }
+
+ public boolean isGlobal() {
+ return global;
+ }
+
+ @Override
+ public DefaultSensorDescriptor name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public DefaultSensorDescriptor onlyOnLanguage(String languageKey) {
+ return onlyOnLanguages(languageKey);
+ }
+
+ @Override
+ public DefaultSensorDescriptor onlyOnLanguages(String... languageKeys) {
+ this.languages = languageKeys;
+ return this;
+ }
+
+ @Override
+ public DefaultSensorDescriptor onlyOnFileType(InputFile.Type type) {
+ this.type = type;
+ return this;
+ }
+
+ @Override
+ public DefaultSensorDescriptor createIssuesForRuleRepository(String... repositoryKey) {
+ return createIssuesForRuleRepositories(repositoryKey);
+ }
+
+ @Override
+ public DefaultSensorDescriptor createIssuesForRuleRepositories(String... repositoryKeys) {
+ this.ruleRepositories = repositoryKeys;
+ return this;
+ }
+
+ @Override
+ public DefaultSensorDescriptor requireProperty(String... propertyKey) {
+ return requireProperties(propertyKey);
+ }
+
+ @Override
+ public DefaultSensorDescriptor requireProperties(String... propertyKeys) {
+ this.configurationPredicate = config -> asList(propertyKeys).stream().allMatch(config::hasKey);
+ return this;
+ }
+
+ @Override
+ public SensorDescriptor global() {
+ this.global = true;
+ return this;
+ }
+
+ @Override
+ public SensorDescriptor onlyWhenConfiguration(Predicate<Configuration> configurationPredicate) {
+ this.configurationPredicate = configurationPredicate;
+ return this;
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSignificantCode.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSignificantCode.java
new file mode 100644
index 00000000000..6a373ac061c
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSignificantCode.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.util.SortedMap;
+import java.util.TreeMap;
+import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.sensor.code.NewSignificantCode;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.utils.Preconditions;
+
+public class DefaultSignificantCode extends DefaultStorable implements NewSignificantCode {
+ private SortedMap<Integer, TextRange> significantCodePerLine = new TreeMap<>();
+ private InputFile inputFile;
+
+ public DefaultSignificantCode() {
+ super();
+ }
+
+ public DefaultSignificantCode(@Nullable SensorStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public DefaultSignificantCode onFile(InputFile inputFile) {
+ this.inputFile = inputFile;
+ return this;
+ }
+
+ @Override
+ public DefaultSignificantCode addRange(TextRange range) {
+ Preconditions.checkState(this.inputFile != null, "addRange() should be called after on()");
+
+ int line = range.start().line();
+
+ Preconditions.checkArgument(line == range.end().line(), "Ranges of significant code must be located in a single line");
+ Preconditions.checkState(!significantCodePerLine.containsKey(line), "Significant code was already reported for line '%s'. Can only report once per line.", line);
+
+ significantCodePerLine.put(line, range);
+ return this;
+ }
+
+ @Override
+ protected void doSave() {
+ Preconditions.checkState(inputFile != null, "Call onFile() first");
+ storage.store(this);
+ }
+
+ public InputFile inputFile() {
+ return inputFile;
+ }
+
+ public SortedMap<Integer, TextRange> significantCodePerLine() {
+ return significantCodePerLine;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultStorable.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultStorable.java
new file mode 100644
index 00000000000..c4f6ca39f4a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultStorable.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import javax.annotation.Nullable;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public abstract class DefaultStorable {
+
+ protected final transient SensorStorage storage;
+ private transient boolean saved = false;
+
+ public DefaultStorable() {
+ this.storage = null;
+ }
+
+ public DefaultStorable(@Nullable SensorStorage storage) {
+ this.storage = storage;
+ }
+
+ public final void save() {
+ requireNonNull(this.storage, "No persister on this object");
+ checkState(!saved, "This object was already saved");
+ doSave();
+ this.saved = true;
+ }
+
+ protected abstract void doSave();
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSymbolTable.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSymbolTable.java
new file mode 100644
index 00000000000..73ec86450a5
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/DefaultSymbolTable.java
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.symbol.NewSymbol;
+import org.sonar.api.batch.sensor.symbol.NewSymbolTable;
+import org.sonar.api.impl.fs.DefaultInputFile;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultSymbolTable extends DefaultStorable implements NewSymbolTable {
+
+ private final Map<TextRange, Set<TextRange>> referencesBySymbol;
+ private DefaultInputFile inputFile;
+
+ public DefaultSymbolTable(SensorStorage storage) {
+ super(storage);
+ referencesBySymbol = new LinkedHashMap<>();
+ }
+
+ public Map<TextRange, Set<TextRange>> getReferencesBySymbol() {
+ return referencesBySymbol;
+ }
+
+ @Override
+ public DefaultSymbolTable onFile(InputFile inputFile) {
+ requireNonNull(inputFile, "file can't be null");
+ this.inputFile = (DefaultInputFile) inputFile;
+ return this;
+ }
+
+ public InputFile inputFile() {
+ return inputFile;
+ }
+
+ @Override
+ public NewSymbol newSymbol(int startLine, int startLineOffset, int endLine, int endLineOffset) {
+ checkInputFileNotNull();
+ TextRange declarationRange;
+ try {
+ declarationRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to create symbol on file " + inputFile, e);
+ }
+ return newSymbol(declarationRange);
+ }
+
+ @Override
+ public NewSymbol newSymbol(int startOffset, int endOffset) {
+ checkInputFileNotNull();
+ TextRange declarationRange;
+ try {
+ declarationRange = inputFile.newRange(startOffset, endOffset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to create symbol on file " + inputFile, e);
+ }
+ return newSymbol(declarationRange);
+ }
+
+ @Override
+ public NewSymbol newSymbol(TextRange range) {
+ checkInputFileNotNull();
+ TreeSet<TextRange> references = new TreeSet<>((o1, o2) -> o1.start().compareTo(o2.start()));
+ referencesBySymbol.put(range, references);
+ return new DefaultSymbol(inputFile, range, references);
+ }
+
+ private static class DefaultSymbol implements NewSymbol {
+
+ private final Collection<TextRange> references;
+ private final DefaultInputFile inputFile;
+ private final TextRange declaration;
+
+ public DefaultSymbol(DefaultInputFile inputFile, TextRange declaration, Collection<TextRange> references) {
+ this.inputFile = inputFile;
+ this.declaration = declaration;
+ this.references = references;
+ }
+
+ @Override
+ public NewSymbol newReference(int startOffset, int endOffset) {
+ TextRange referenceRange;
+ try {
+ referenceRange = inputFile.newRange(startOffset, endOffset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to create symbol reference on file " + inputFile, e);
+ }
+ return newReference(referenceRange);
+ }
+
+ @Override
+ public NewSymbol newReference(int startLine, int startLineOffset, int endLine, int endLineOffset) {
+ TextRange referenceRange;
+ try {
+ referenceRange = inputFile.newRange(startLine, startLineOffset, endLine, endLineOffset);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to create symbol reference on file " + inputFile, e);
+ }
+ return newReference(referenceRange);
+ }
+
+ @Override
+ public NewSymbol newReference(TextRange range) {
+ requireNonNull(range, "Provided range is null");
+ checkArgument(!declaration.overlap(range), "Overlapping symbol declaration and reference for symbol at %s", declaration);
+ references.add(range);
+ return this;
+ }
+
+ }
+
+ @Override
+ protected void doSave() {
+ checkInputFileNotNull();
+ storage.store(this);
+ }
+
+ private void checkInputFileNotNull() {
+ checkState(inputFile != null, "Call onFile() first");
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/InMemorySensorStorage.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/InMemorySensorStorage.java
new file mode 100644
index 00000000000..26f74db0341
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/InMemorySensorStorage.java
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+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 org.sonar.api.batch.sensor.code.NewSignificantCode;
+import org.sonar.api.batch.sensor.coverage.NewCoverage;
+import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
+import org.sonar.api.batch.sensor.error.AnalysisError;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.batch.sensor.internal.SensorStorage;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
+import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.measure.Measure;
+import org.sonar.api.batch.sensor.rule.AdHocRule;
+import org.sonar.api.batch.sensor.symbol.NewSymbolTable;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+class InMemorySensorStorage implements SensorStorage {
+
+ Map<String, Map<String, Measure>> measuresByComponentAndMetric = new HashMap<>();
+
+ Collection<Issue> allIssues = new ArrayList<>();
+ Collection<ExternalIssue> allExternalIssues = new ArrayList<>();
+ Collection<AdHocRule> allAdHocRules = new ArrayList<>();
+ Collection<AnalysisError> allAnalysisErrors = new ArrayList<>();
+
+ Map<String, NewHighlighting> highlightingByComponent = new HashMap<>();
+ Map<String, DefaultCpdTokens> cpdTokensByComponent = new HashMap<>();
+ Map<String, List<DefaultCoverage>> coverageByComponent = new HashMap<>();
+ Map<String, DefaultSymbolTable> symbolsPerComponent = new HashMap<>();
+ Map<String, String> contextProperties = new HashMap<>();
+ Map<String, DefaultSignificantCode> significantCodePerComponent = new HashMap<>();
+
+ @Override
+ public void store(Measure measure) {
+ // Emulate duplicate measure check
+ String componentKey = measure.inputComponent().key();
+ String metricKey = measure.metric().key();
+ if (measuresByComponentAndMetric.getOrDefault(componentKey, Collections.emptyMap()).containsKey(metricKey)) {
+ throw new IllegalStateException("Can not add the same measure twice");
+ }
+ measuresByComponentAndMetric.computeIfAbsent(componentKey, x -> new HashMap<>()).put(metricKey, measure);
+ }
+
+ @Override
+ public void store(Issue issue) {
+ allIssues.add(issue);
+ }
+
+ @Override
+ public void store(AdHocRule adHocRule) {
+ allAdHocRules.add(adHocRule);
+ }
+
+ @Override
+ public void store(NewHighlighting newHighlighting) {
+ DefaultHighlighting highlighting = (DefaultHighlighting) newHighlighting;
+ String fileKey = highlighting.inputFile().key();
+ // Emulate duplicate storage check
+ if (highlightingByComponent.containsKey(fileKey)) {
+ throw new UnsupportedOperationException("Trying to save highlighting twice for the same file is not supported: " + highlighting.inputFile());
+ }
+ highlightingByComponent.put(fileKey, highlighting);
+ }
+
+ @Override
+ public void store(NewCoverage coverage) {
+ DefaultCoverage defaultCoverage = (DefaultCoverage) coverage;
+ String fileKey = defaultCoverage.inputFile().key();
+ coverageByComponent.computeIfAbsent(fileKey, x -> new ArrayList<>()).add(defaultCoverage);
+ }
+
+ @Override
+ public void store(NewCpdTokens cpdTokens) {
+ DefaultCpdTokens defaultCpdTokens = (DefaultCpdTokens) cpdTokens;
+ String fileKey = defaultCpdTokens.inputFile().key();
+ // Emulate duplicate storage check
+ if (cpdTokensByComponent.containsKey(fileKey)) {
+ throw new UnsupportedOperationException("Trying to save CPD tokens twice for the same file is not supported: " + defaultCpdTokens.inputFile());
+ }
+ cpdTokensByComponent.put(fileKey, defaultCpdTokens);
+ }
+
+ @Override
+ public void store(NewSymbolTable newSymbolTable) {
+ DefaultSymbolTable symbolTable = (DefaultSymbolTable) newSymbolTable;
+ String fileKey = symbolTable.inputFile().key();
+ // Emulate duplicate storage check
+ if (symbolsPerComponent.containsKey(fileKey)) {
+ throw new UnsupportedOperationException("Trying to save symbol table twice for the same file is not supported: " + symbolTable.inputFile());
+ }
+ symbolsPerComponent.put(fileKey, symbolTable);
+ }
+
+ @Override
+ public void store(AnalysisError analysisError) {
+ allAnalysisErrors.add(analysisError);
+ }
+
+ @Override
+ public void storeProperty(String key, String value) {
+ checkArgument(key != null, "Key of context property must not be null");
+ checkArgument(value != null, "Value of context property must not be null");
+ contextProperties.put(key, value);
+ }
+
+ @Override
+ public void store(ExternalIssue issue) {
+ allExternalIssues.add(issue);
+ }
+
+ @Override
+ public void store(NewSignificantCode newSignificantCode) {
+ DefaultSignificantCode significantCode = (DefaultSignificantCode) newSignificantCode;
+ String fileKey = significantCode.inputFile().key();
+ // Emulate duplicate storage check
+ if (significantCodePerComponent.containsKey(fileKey)) {
+ throw new UnsupportedOperationException("Trying to save significant code information twice for the same file is not supported: " + significantCode.inputFile());
+ }
+ significantCodePerComponent.put(fileKey, significantCode);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SensorContextTester.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SensorContextTester.java
new file mode 100644
index 00000000000..6877a771175
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SensorContextTester.java
@@ -0,0 +1,399 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import java.io.File;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.InputModule;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.code.NewSignificantCode;
+import org.sonar.api.batch.sensor.coverage.NewCoverage;
+import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
+import org.sonar.api.batch.sensor.cpd.internal.TokensLine;
+import org.sonar.api.batch.sensor.error.AnalysisError;
+import org.sonar.api.batch.sensor.error.NewAnalysisError;
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+import org.sonar.api.batch.sensor.issue.ExternalIssue;
+import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.batch.sensor.issue.NewExternalIssue;
+import org.sonar.api.batch.sensor.issue.NewIssue;
+import org.sonar.api.batch.sensor.measure.Measure;
+import org.sonar.api.batch.sensor.measure.NewMeasure;
+import org.sonar.api.batch.sensor.rule.AdHocRule;
+import org.sonar.api.batch.sensor.rule.NewAdHocRule;
+import org.sonar.api.batch.sensor.symbol.NewSymbolTable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.Settings;
+import org.sonar.api.impl.config.ConfigurationBridge;
+import org.sonar.api.impl.config.MapSettings;
+import org.sonar.api.impl.fs.DefaultFileSystem;
+import org.sonar.api.impl.fs.DefaultInputFile;
+import org.sonar.api.impl.fs.DefaultInputModule;
+import org.sonar.api.impl.fs.DefaultInputProject;
+import org.sonar.api.impl.fs.DefaultTextPointer;
+import org.sonar.api.impl.issue.DefaultIssue;
+import org.sonar.api.impl.rule.ActiveRulesBuilder;
+import org.sonar.api.impl.context.MetadataLoader;
+import org.sonar.api.impl.context.SonarRuntimeImpl;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.scanner.fs.InputProject;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
+
+import static java.util.Collections.unmodifiableMap;
+
+/**
+ * Utility class to help testing {@link Sensor}. This is not an API and method signature may evolve.
+ * <p>
+ * Usage: call {@link #create(File)} to create an "in memory" implementation of {@link SensorContext} with a filesystem initialized with provided baseDir.
+ * <p>
+ * You have to manually register inputFiles using:
+ * <pre>
+ * sensorContextTester.fileSystem().add(new DefaultInputFile("myProjectKey", "src/Foo.java")
+ * .setLanguage("java")
+ * .initMetadata("public class Foo {\n}"));
+ * </pre>
+ * <p>
+ * Then pass it to your {@link Sensor}. You can then query elements provided by your sensor using methods {@link #allIssues()}, ...
+ */
+public class SensorContextTester implements SensorContext {
+
+ private Settings settings;
+ private DefaultFileSystem fs;
+ private ActiveRules activeRules;
+ private InMemorySensorStorage sensorStorage;
+ private DefaultInputProject project;
+ private DefaultInputModule module;
+ private SonarRuntime runtime;
+ private boolean cancelled;
+
+ private SensorContextTester(Path moduleBaseDir) {
+ this.settings = new MapSettings();
+ this.fs = new DefaultFileSystem(moduleBaseDir).setEncoding(Charset.defaultCharset());
+ this.activeRules = new ActiveRulesBuilder().build();
+ this.sensorStorage = new InMemorySensorStorage();
+ this.project = new DefaultInputProject(ProjectDefinition.create().setKey("projectKey").setBaseDir(moduleBaseDir.toFile()).setWorkDir(moduleBaseDir.resolve(".sonar").toFile()));
+ this.module = new DefaultInputModule(ProjectDefinition.create().setKey("projectKey").setBaseDir(moduleBaseDir.toFile()).setWorkDir(moduleBaseDir.resolve(".sonar").toFile()));
+ this.runtime = SonarRuntimeImpl.forSonarQube(MetadataLoader.loadVersion(System2.INSTANCE), SonarQubeSide.SCANNER, MetadataLoader.loadEdition(System2.INSTANCE));
+ }
+
+ public static SensorContextTester create(File moduleBaseDir) {
+ return new SensorContextTester(moduleBaseDir.toPath());
+ }
+
+ public static SensorContextTester create(Path moduleBaseDir) {
+ return new SensorContextTester(moduleBaseDir);
+ }
+
+ @Override
+ public Settings settings() {
+ return settings;
+ }
+
+ @Override
+ public Configuration config() {
+ return new ConfigurationBridge(settings);
+ }
+
+ public SensorContextTester setSettings(Settings settings) {
+ this.settings = settings;
+ return this;
+ }
+
+ @Override
+ public DefaultFileSystem fileSystem() {
+ return fs;
+ }
+
+ public SensorContextTester setFileSystem(DefaultFileSystem fs) {
+ this.fs = fs;
+ return this;
+ }
+
+ @Override
+ public ActiveRules activeRules() {
+ return activeRules;
+ }
+
+ public SensorContextTester setActiveRules(ActiveRules activeRules) {
+ this.activeRules = activeRules;
+ return this;
+ }
+
+ /**
+ * Default value is the version of this API at compilation time. You can override it
+ * using {@link #setRuntime(SonarRuntime)} to test your Sensor behaviour.
+ */
+ @Override
+ public Version getSonarQubeVersion() {
+ return runtime().getApiVersion();
+ }
+
+ /**
+ * @see #setRuntime(SonarRuntime) to override defaults (SonarQube scanner with version
+ * of this API as used at compilation time).
+ */
+ @Override
+ public SonarRuntime runtime() {
+ return runtime;
+ }
+
+ public SensorContextTester setRuntime(SonarRuntime runtime) {
+ this.runtime = runtime;
+ return this;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ public InputModule module() {
+ return module;
+ }
+
+ @Override
+ public InputProject project() {
+ return project;
+ }
+
+ @Override
+ public <G extends Serializable> NewMeasure<G> newMeasure() {
+ return new DefaultMeasure<>(sensorStorage);
+ }
+
+ public Collection<Measure> measures(String componentKey) {
+ return sensorStorage.measuresByComponentAndMetric.getOrDefault(componentKey, Collections.emptyMap()).values();
+ }
+
+ public <G extends Serializable> Measure<G> measure(String componentKey, Metric<G> metric) {
+ return measure(componentKey, metric.key());
+ }
+
+ public <G extends Serializable> Measure<G> measure(String componentKey, String metricKey) {
+ return sensorStorage.measuresByComponentAndMetric.getOrDefault(componentKey, Collections.emptyMap()).get(metricKey);
+ }
+
+ @Override
+ public NewIssue newIssue() {
+ return new DefaultIssue(project, sensorStorage);
+ }
+
+ public Collection<Issue> allIssues() {
+ return sensorStorage.allIssues;
+ }
+
+ @Override
+ public NewExternalIssue newExternalIssue() {
+ return new DefaultExternalIssue(project, sensorStorage);
+ }
+
+ @Override
+ public NewAdHocRule newAdHocRule() {
+ return new DefaultAdHocRule(sensorStorage);
+ }
+
+ public Collection<ExternalIssue> allExternalIssues() {
+ return sensorStorage.allExternalIssues;
+ }
+
+ public Collection<AdHocRule> allAdHocRules() {
+ return sensorStorage.allAdHocRules;
+ }
+
+ public Collection<AnalysisError> allAnalysisErrors() {
+ return sensorStorage.allAnalysisErrors;
+ }
+
+ @CheckForNull
+ public Integer lineHits(String fileKey, int line) {
+ return sensorStorage.coverageByComponent.getOrDefault(fileKey, Collections.emptyList()).stream()
+ .map(c -> c.hitsByLine().get(line))
+ .flatMap(Stream::of)
+ .filter(Objects::nonNull)
+ .reduce(null, SensorContextTester::sumOrNull);
+ }
+
+ @CheckForNull
+ public static Integer sumOrNull(@Nullable Integer o1, @Nullable Integer o2) {
+ return o1 == null ? o2 : (o1 + o2);
+ }
+
+ @CheckForNull
+ public Integer conditions(String fileKey, int line) {
+ return sensorStorage.coverageByComponent.getOrDefault(fileKey, Collections.emptyList()).stream()
+ .map(c -> c.conditionsByLine().get(line))
+ .flatMap(Stream::of)
+ .filter(Objects::nonNull)
+ .reduce(null, SensorContextTester::maxOrNull);
+ }
+
+ @CheckForNull
+ public Integer coveredConditions(String fileKey, int line) {
+ return sensorStorage.coverageByComponent.getOrDefault(fileKey, Collections.emptyList()).stream()
+ .map(c -> c.coveredConditionsByLine().get(line))
+ .flatMap(Stream::of)
+ .filter(Objects::nonNull)
+ .reduce(null, SensorContextTester::maxOrNull);
+ }
+
+ @CheckForNull
+ public TextRange significantCodeTextRange(String fileKey, int line) {
+ if (sensorStorage.significantCodePerComponent.containsKey(fileKey)) {
+ return sensorStorage.significantCodePerComponent.get(fileKey)
+ .significantCodePerLine()
+ .get(line);
+ }
+ return null;
+
+ }
+
+ @CheckForNull
+ public static Integer maxOrNull(@Nullable Integer o1, @Nullable Integer o2) {
+ return o1 == null ? o2 : Math.max(o1, o2);
+ }
+
+ @CheckForNull
+ public List<TokensLine> cpdTokens(String componentKey) {
+ DefaultCpdTokens defaultCpdTokens = sensorStorage.cpdTokensByComponent.get(componentKey);
+ return defaultCpdTokens != null ? defaultCpdTokens.getTokenLines() : null;
+ }
+
+ @Override
+ public NewHighlighting newHighlighting() {
+ return new DefaultHighlighting(sensorStorage);
+ }
+
+ @Override
+ public NewCoverage newCoverage() {
+ return new DefaultCoverage(sensorStorage);
+ }
+
+ @Override
+ public NewCpdTokens newCpdTokens() {
+ return new DefaultCpdTokens(sensorStorage);
+ }
+
+ @Override
+ public NewSymbolTable newSymbolTable() {
+ return new DefaultSymbolTable(sensorStorage);
+ }
+
+ @Override
+ public NewAnalysisError newAnalysisError() {
+ return new DefaultAnalysisError(sensorStorage);
+ }
+
+ /**
+ * Return list of syntax highlighting applied for a given position in a file. The result is a list because in theory you
+ * can apply several styles to the same range.
+ *
+ * @param componentKey Key of the file like 'myProjectKey:src/foo.php'
+ * @param line Line you want to query
+ * @param lineOffset Offset you want to query.
+ * @return List of styles applied to this position or empty list if there is no highlighting at this position.
+ */
+ public List<TypeOfText> highlightingTypeAt(String componentKey, int line, int lineOffset) {
+ DefaultHighlighting syntaxHighlightingData = (DefaultHighlighting) sensorStorage.highlightingByComponent.get(componentKey);
+ if (syntaxHighlightingData == null) {
+ return Collections.emptyList();
+ }
+ List<TypeOfText> result = new ArrayList<>();
+ DefaultTextPointer location = new DefaultTextPointer(line, lineOffset);
+ for (SyntaxHighlightingRule sortedRule : syntaxHighlightingData.getSyntaxHighlightingRuleSet()) {
+ if (sortedRule.range().start().compareTo(location) <= 0 && sortedRule.range().end().compareTo(location) > 0) {
+ result.add(sortedRule.getTextType());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Return list of symbol references ranges for the symbol at a given position in a file.
+ *
+ * @param componentKey Key of the file like 'myProjectKey:src/foo.php'
+ * @param line Line you want to query
+ * @param lineOffset Offset you want to query.
+ * @return List of references for the symbol (potentially empty) or null if there is no symbol at this position.
+ */
+ @CheckForNull
+ public Collection<TextRange> referencesForSymbolAt(String componentKey, int line, int lineOffset) {
+ DefaultSymbolTable symbolTable = sensorStorage.symbolsPerComponent.get(componentKey);
+ if (symbolTable == null) {
+ return null;
+ }
+ DefaultTextPointer location = new DefaultTextPointer(line, lineOffset);
+ for (Map.Entry<TextRange, Set<TextRange>> symbol : symbolTable.getReferencesBySymbol().entrySet()) {
+ if (symbol.getKey().start().compareTo(location) <= 0 && symbol.getKey().end().compareTo(location) > 0) {
+ return symbol.getValue();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void addContextProperty(String key, String value) {
+ sensorStorage.storeProperty(key, value);
+ }
+
+ /**
+ * @return an immutable map of the context properties defined with {@link SensorContext#addContextProperty(String, String)}.
+ * @since 6.1
+ */
+ public Map<String, String> getContextProperties() {
+ return unmodifiableMap(sensorStorage.contextProperties);
+ }
+
+ @Override
+ public void markForPublishing(InputFile inputFile) {
+ DefaultInputFile file = (DefaultInputFile) inputFile;
+ file.setPublished(true);
+ }
+
+ @Override
+ public NewSignificantCode newSignificantCode() {
+ return new DefaultSignificantCode(sensorStorage);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SyntaxHighlightingRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SyntaxHighlightingRule.java
new file mode 100644
index 00000000000..35d4fbea48a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/SyntaxHighlightingRule.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.sensor;
+
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.sensor.highlighting.TypeOfText;
+
+public class SyntaxHighlightingRule {
+
+ private final TextRange range;
+ private final TypeOfText textType;
+
+ private SyntaxHighlightingRule(TextRange range, TypeOfText textType) {
+ this.range = range;
+ this.textType = textType;
+ }
+
+ public static SyntaxHighlightingRule create(TextRange range, TypeOfText textType) {
+ return new SyntaxHighlightingRule(range, textType);
+ }
+
+ public TextRange range() {
+ return range;
+ }
+
+ public TypeOfText getTextType() {
+ return textType;
+ }
+
+ @Override
+ public String toString() {
+ return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/package-info.java
new file mode 100644
index 00000000000..b304d3197d8
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/sensor/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.sensor;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultDebtRemediationFunctions.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultDebtRemediationFunctions.java
new file mode 100644
index 00000000000..52bf1206057
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultDebtRemediationFunctions.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import javax.annotation.Nullable;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.debt.internal.DefaultDebtRemediationFunction;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.MessageException;
+
+/**
+ * Factory of {@link org.sonar.api.server.debt.DebtRemediationFunction} that keeps
+ * a context of rule for better error messages. Used only when declaring rules.
+ *
+ * @see org.sonar.api.server.rule.RulesDefinition
+ */
+class DefaultDebtRemediationFunctions implements RulesDefinition.DebtRemediationFunctions {
+
+ private final String repoKey;
+ private final String key;
+
+ DefaultDebtRemediationFunctions(String repoKey, String key) {
+ this.repoKey = repoKey;
+ this.key = key;
+ }
+
+ @Override
+ public DebtRemediationFunction linear(String gapMultiplier) {
+ return create(DefaultDebtRemediationFunction.Type.LINEAR, gapMultiplier, null);
+ }
+
+ @Override
+ public DebtRemediationFunction linearWithOffset(String gapMultiplier, String baseEffort) {
+ return create(DefaultDebtRemediationFunction.Type.LINEAR_OFFSET, gapMultiplier, baseEffort);
+ }
+
+ @Override
+ public DebtRemediationFunction constantPerIssue(String baseEffort) {
+ return create(DefaultDebtRemediationFunction.Type.CONSTANT_ISSUE, null, baseEffort);
+ }
+
+ @Override
+ public DebtRemediationFunction create(DebtRemediationFunction.Type type, @Nullable String gapMultiplier, @Nullable String baseEffort) {
+ try {
+ return new DefaultDebtRemediationFunction(type, gapMultiplier, baseEffort);
+ } catch (Exception e) {
+ throw MessageException.of(String.format("The rule '%s:%s' is invalid : %s ", this.repoKey, this.key, e.getMessage()));
+ }
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewParam.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewParam.java
new file mode 100644
index 00000000000..6a026db8934
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewParam.java
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static org.apache.commons.lang.StringUtils.defaultIfEmpty;
+
+public class DefaultNewParam extends RulesDefinition.NewParam {
+ private final String key;
+ private String name;
+ private String description;
+ private String defaultValue;
+ private RuleParamType type = RuleParamType.STRING;
+
+ DefaultNewParam(String key) {
+ this.key = this.name = key;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public DefaultNewParam setName(@Nullable String s) {
+ // name must never be null.
+ this.name = StringUtils.defaultIfBlank(s, key);
+ return this;
+ }
+
+ @Override
+ public DefaultNewParam setType(RuleParamType t) {
+ this.type = t;
+ return this;
+ }
+
+ @Override
+ public DefaultNewParam setDescription(@Nullable String s) {
+ this.description = StringUtils.defaultIfBlank(s, null);
+ return this;
+ }
+
+ @Override
+ public DefaultNewParam setDefaultValue(@Nullable String s) {
+ this.defaultValue = defaultIfEmpty(s, null);
+ return this;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String description() {
+ return description;
+ }
+
+ public String defaultValue() {
+ return defaultValue;
+ }
+
+ public RuleParamType type() {
+ return type;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRepository.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRepository.java
new file mode 100644
index 00000000000..9fe35843b8c
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRepository.java
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+public class DefaultNewRepository implements RulesDefinition.NewRepository {
+ private final RuleDefinitionContext context;
+ private final String key;
+ private final boolean isExternal;
+ private final String language;
+ private String name;
+ private final Map<String, RulesDefinition.NewRule> newRules = new HashMap<>();
+
+ DefaultNewRepository(RuleDefinitionContext context, String key, String language, boolean isExternal) {
+ this.context = context;
+ this.key = key;
+ this.name = key;
+ this.language = language;
+ this.isExternal = isExternal;
+ }
+
+ @Override
+ public boolean isExternal() {
+ return isExternal;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ String language() {
+ return language;
+ }
+
+ Map<String, RulesDefinition.NewRule> newRules() {
+ return newRules;
+ }
+
+ String name() {
+ return name;
+ }
+
+ @Override
+ public DefaultNewRepository setName(@Nullable String s) {
+ if (StringUtils.isNotEmpty(s)) {
+ this.name = s;
+ }
+ return this;
+ }
+
+ @Override
+ public RulesDefinition.NewRule createRule(String ruleKey) {
+ checkArgument(!newRules.containsKey(ruleKey), "The rule '%s' of repository '%s' is declared several times", ruleKey, key);
+ RulesDefinition.NewRule newRule = new DefaultNewRule(context.currentPluginKey(), key, ruleKey);
+ newRules.put(ruleKey, newRule);
+ return newRule;
+ }
+
+ @CheckForNull
+ @Override
+ public RulesDefinition.NewRule rule(String ruleKey) {
+ return newRules.get(ruleKey);
+ }
+
+ @Override
+ public Collection<RulesDefinition.NewRule> rules() {
+ return newRules.values();
+ }
+
+ @Override
+ public void done() {
+ // note that some validations can be done here, for example for
+ // verifying that at least one rule is declared
+
+ context.registerRepository(this);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("NewRepository{");
+ sb.append("key='").append(key).append('\'');
+ sb.append(", language='").append(language).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRule.java
new file mode 100644
index 00000000000..465b8f97bc2
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultNewRule.java
@@ -0,0 +1,350 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleScope;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.RuleTagFormat;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.lang.StringUtils.isEmpty;
+import static org.apache.commons.lang.StringUtils.trimToNull;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+class DefaultNewRule extends RulesDefinition.NewRule {
+ private final String pluginKey;
+ private final String repoKey;
+ private final String key;
+ private RuleType type;
+ private String name;
+ private String htmlDescription;
+ private String markdownDescription;
+ private String internalKey;
+ private String severity = Severity.MAJOR;
+ private boolean template;
+ private RuleStatus status = RuleStatus.defaultStatus();
+ private DebtRemediationFunction debtRemediationFunction;
+ private String gapDescription;
+ private final Set<String> tags = new TreeSet<>();
+ private final Set<String> securityStandards = new TreeSet<>();
+ private final Map<String, RulesDefinition.NewParam> paramsByKey = new HashMap<>();
+ private final RulesDefinition.DebtRemediationFunctions functions;
+ private boolean activatedByDefault;
+ private RuleScope scope;
+ private final Set<RuleKey> deprecatedRuleKeys = new TreeSet<>();
+
+ DefaultNewRule(@Nullable String pluginKey, String repoKey, String key) {
+ this.pluginKey = pluginKey;
+ this.repoKey = repoKey;
+ this.key = key;
+ this.functions = new DefaultDebtRemediationFunctions(repoKey, key);
+ }
+
+ @Override
+ public String key() {
+ return this.key;
+ }
+
+ @CheckForNull
+ @Override
+ public RuleScope scope() {
+ return this.scope;
+ }
+
+ @Override
+ public DefaultNewRule setScope(RuleScope scope) {
+ this.scope = scope;
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setName(String s) {
+ this.name = trimToNull(s);
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setTemplate(boolean template) {
+ this.template = template;
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setActivatedByDefault(boolean activatedByDefault) {
+ this.activatedByDefault = activatedByDefault;
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setSeverity(String s) {
+ checkArgument(Severity.ALL.contains(s), "Severity of rule %s is not correct: %s", this, s);
+ this.severity = s;
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setType(RuleType t) {
+ this.type = t;
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setHtmlDescription(@Nullable String s) {
+ checkState(markdownDescription == null, "Rule '%s' already has a Markdown description", this);
+ this.htmlDescription = trimToNull(s);
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setHtmlDescription(@Nullable URL classpathUrl) {
+ if (classpathUrl != null) {
+ try {
+ setHtmlDescription(IOUtils.toString(classpathUrl, UTF_8));
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to read: " + classpathUrl, e);
+ }
+ } else {
+ this.htmlDescription = null;
+ }
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setMarkdownDescription(@Nullable String s) {
+ checkState(htmlDescription == null, "Rule '%s' already has an HTML description", this);
+ this.markdownDescription = trimToNull(s);
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setMarkdownDescription(@Nullable URL classpathUrl) {
+ if (classpathUrl != null) {
+ try {
+ setMarkdownDescription(IOUtils.toString(classpathUrl, UTF_8));
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to read: " + classpathUrl, e);
+ }
+ } else {
+ this.markdownDescription = null;
+ }
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setStatus(RuleStatus status) {
+ checkArgument(RuleStatus.REMOVED != status, "Status 'REMOVED' is not accepted on rule '%s'", this);
+ this.status = status;
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setDebtSubCharacteristic(@Nullable String s) {
+ return this;
+ }
+
+ @Override
+ public RulesDefinition.DebtRemediationFunctions debtRemediationFunctions() {
+ return functions;
+ }
+
+ @Override
+ public DefaultNewRule setDebtRemediationFunction(@Nullable DebtRemediationFunction fn) {
+ this.debtRemediationFunction = fn;
+ return this;
+ }
+
+ @Deprecated
+ @Override
+ public DefaultNewRule setEffortToFixDescription(@Nullable String s) {
+ return setGapDescription(s);
+ }
+
+ @Override
+ public DefaultNewRule setGapDescription(@Nullable String s) {
+ this.gapDescription = s;
+ return this;
+ }
+
+ @Override
+ public RulesDefinition.NewParam createParam(String paramKey) {
+ checkArgument(!paramsByKey.containsKey(paramKey), "The parameter '%s' is declared several times on the rule %s", paramKey, this);
+ DefaultNewParam param = new DefaultNewParam(paramKey);
+ paramsByKey.put(paramKey, param);
+ return param;
+ }
+
+ @CheckForNull
+ @Override
+ public RulesDefinition.NewParam param(String paramKey) {
+ return paramsByKey.get(paramKey);
+ }
+
+ @Override
+ public Collection<RulesDefinition.NewParam> params() {
+ return paramsByKey.values();
+ }
+
+ @Override
+ public DefaultNewRule addTags(String... list) {
+ for (String tag : list) {
+ RuleTagFormat.validate(tag);
+ tags.add(tag);
+ }
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setTags(String... list) {
+ tags.clear();
+ addTags(list);
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule addOwaspTop10(RulesDefinition.OwaspTop10... standards) {
+ for (RulesDefinition.OwaspTop10 owaspTop10 : standards) {
+ String standard = "owaspTop10:" + owaspTop10.name().toLowerCase(Locale.ENGLISH);
+ securityStandards.add(standard);
+ }
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule addCwe(int... nums) {
+ for (int num : nums) {
+ String standard = "cwe:" + num;
+ securityStandards.add(standard);
+ }
+ return this;
+ }
+
+ @Override
+ public DefaultNewRule setInternalKey(@Nullable String s) {
+ this.internalKey = s;
+ return this;
+ }
+
+ void validate() {
+ if (isEmpty(name)) {
+ throw new IllegalStateException(format("Name of rule %s is empty", this));
+ }
+ if (isEmpty(htmlDescription) && isEmpty(markdownDescription)) {
+ throw new IllegalStateException(format("One of HTML description or Markdown description must be defined for rule %s", this));
+ }
+ }
+
+ @Override
+ public DefaultNewRule addDeprecatedRuleKey(String repository, String key) {
+ deprecatedRuleKeys.add(RuleKey.of(repository, key));
+ return this;
+ }
+
+ String pluginKey() {
+ return pluginKey;
+ }
+
+ String repoKey() {
+ return repoKey;
+ }
+
+ RuleType type() {
+ return type;
+ }
+
+ String name() {
+ return name;
+ }
+
+ String htmlDescription() {
+ return htmlDescription;
+ }
+
+ String markdownDescription() {
+ return markdownDescription;
+ }
+
+ @CheckForNull
+ String internalKey() {
+ return internalKey;
+ }
+
+ String severity() {
+ return severity;
+ }
+
+ boolean template() {
+ return template;
+ }
+
+ RuleStatus status() {
+ return status;
+ }
+
+ DebtRemediationFunction debtRemediationFunction() {
+ return debtRemediationFunction;
+ }
+
+ String gapDescription() {
+ return gapDescription;
+ }
+
+ Set<String> tags() {
+ return tags;
+ }
+
+ Set<String> securityStandards() {
+ return securityStandards;
+ }
+
+ Map<String, RulesDefinition.NewParam> paramsByKey() {
+ return paramsByKey;
+ }
+
+ boolean activatedByDefault() {
+ return activatedByDefault;
+ }
+
+ Set<RuleKey> deprecatedRuleKeys() {
+ return deprecatedRuleKeys;
+ }
+
+ @Override
+ public String toString() {
+ return format("[repository=%s, key=%s]", repoKey, key);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultParam.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultParam.java
new file mode 100644
index 00000000000..ea414450fdf
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultParam.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.server.rule.RuleParamType;
+import org.sonar.api.server.rule.RulesDefinition;
+
+@Immutable
+public class DefaultParam implements RulesDefinition.Param {
+ private final String key;
+ private final String name;
+ private final String description;
+ private final String defaultValue;
+ private final RuleParamType type;
+
+ DefaultParam(DefaultNewParam newParam) {
+ this.key = newParam.key();
+ this.name = newParam.name();
+ this.description = newParam.description();
+ this.defaultValue = newParam.defaultValue();
+ this.type = newParam.type();
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ @Nullable
+ public String description() {
+ return description;
+ }
+
+ @Override
+ @Nullable
+ public String defaultValue() {
+ return defaultValue;
+ }
+
+ @Override
+ public RuleParamType type() {
+ return type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RulesDefinition.Param that = (RulesDefinition.Param) o;
+ return key.equals(that.key());
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRepository.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRepository.java
new file mode 100644
index 00000000000..8d56b973499
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRepository.java
@@ -0,0 +1,128 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.server.rule.RulesDefinition;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Collections.unmodifiableMap;
+
+@Immutable
+class DefaultRepository implements RulesDefinition.Repository {
+ private final String key;
+ private final String language;
+ private final String name;
+ private final boolean isExternal;
+ private final Map<String, RulesDefinition.Rule> rulesByKey;
+
+ DefaultRepository(DefaultNewRepository newRepository, @Nullable RulesDefinition.Repository mergeInto) {
+ this.key = newRepository.key();
+ this.language = newRepository.language();
+ this.isExternal = newRepository.isExternal();
+ Map<String, RulesDefinition.Rule> ruleBuilder = new HashMap<>();
+ if (mergeInto != null) {
+ if (!StringUtils.equals(newRepository.language(), mergeInto.language()) || !StringUtils.equals(newRepository.key(), mergeInto.key())) {
+ throw new IllegalArgumentException(format("Bug - language and key of the repositories to be merged should be the sames: %s and %s", newRepository, mergeInto));
+ }
+ this.name = StringUtils.defaultIfBlank(mergeInto.name(), newRepository.name());
+ for (RulesDefinition.Rule rule : mergeInto.rules()) {
+ if (!newRepository.key().startsWith("common-") && ruleBuilder.containsKey(rule.key())) {
+ Loggers.get(getClass()).warn("The rule '{}' of repository '{}' is declared several times", rule.key(), mergeInto.key());
+ }
+ ruleBuilder.put(rule.key(), rule);
+ }
+ } else {
+ this.name = newRepository.name();
+ }
+ for (RulesDefinition.NewRule newRule : newRepository.newRules().values()) {
+ DefaultNewRule defaultNewRule = (DefaultNewRule) newRule;
+ defaultNewRule.validate();
+ ruleBuilder.put(newRule.key(), new DefaultRule(this, defaultNewRule));
+ }
+ this.rulesByKey = unmodifiableMap(ruleBuilder);
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String language() {
+ return language;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public boolean isExternal() {
+ return isExternal;
+ }
+
+ @Override
+ @CheckForNull
+ public RulesDefinition.Rule rule(String ruleKey) {
+ return rulesByKey.get(ruleKey);
+ }
+
+ @Override
+ public List<RulesDefinition.Rule> rules() {
+ return unmodifiableList(new ArrayList<>(rulesByKey.values()));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ DefaultRepository that = (DefaultRepository) o;
+ return key.equals(that.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Repository{");
+ sb.append("key='").append(key).append('\'');
+ sb.append(", language='").append(language).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRule.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRule.java
new file mode 100644
index 00000000000..d56201d6635
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/DefaultRule.java
@@ -0,0 +1,238 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.annotation.CheckForNull;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.RuleScope;
+import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.debt.DebtRemediationFunction;
+import org.sonar.api.server.rule.RuleTagsToTypeConverter;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static java.lang.String.format;
+import static java.util.Collections.unmodifiableList;
+
+@Immutable
+public class DefaultRule implements RulesDefinition.Rule {
+ private final String pluginKey;
+ private final RulesDefinition.Repository repository;
+ private final String repoKey;
+ private final String key;
+ private final String name;
+ private final RuleType type;
+ private final String htmlDescription;
+ private final String markdownDescription;
+ private final String internalKey;
+ private final String severity;
+ private final boolean template;
+ private final DebtRemediationFunction debtRemediationFunction;
+ private final String gapDescription;
+ private final Set<String> tags;
+ private final Set<String> securityStandards;
+ private final Map<String, RulesDefinition.Param> params;
+ private final RuleStatus status;
+ private final boolean activatedByDefault;
+ private final RuleScope scope;
+ private final Set<RuleKey> deprecatedRuleKeys;
+
+ DefaultRule(DefaultRepository repository, DefaultNewRule newRule) {
+ this.pluginKey = newRule.pluginKey();
+ this.repository = repository;
+ this.repoKey = newRule.repoKey();
+ this.key = newRule.key();
+ this.name = newRule.name();
+ this.htmlDescription = newRule.htmlDescription();
+ this.markdownDescription = newRule.markdownDescription();
+ this.internalKey = newRule.internalKey();
+ this.severity = newRule.severity();
+ this.template = newRule.template();
+ this.status = newRule.status();
+ this.debtRemediationFunction = newRule.debtRemediationFunction();
+ this.gapDescription = newRule.gapDescription();
+ this.scope = newRule.scope() == null ? RuleScope.MAIN : newRule.scope();
+ this.type = newRule.type() == null ? RuleTagsToTypeConverter.convert(newRule.tags()) : newRule.type();
+ Set<String> tagsBuilder = new TreeSet<>(newRule.tags());
+ tagsBuilder.removeAll(RuleTagsToTypeConverter.RESERVED_TAGS);
+ this.tags = Collections.unmodifiableSet(tagsBuilder);
+ this.securityStandards = Collections.unmodifiableSet(new TreeSet<>(newRule.securityStandards()));
+ Map<String, RulesDefinition.Param> paramsBuilder = new HashMap<>();
+ for (RulesDefinition.NewParam newParam : newRule.paramsByKey().values()) {
+ paramsBuilder.put(newParam.key(), new DefaultParam((DefaultNewParam) newParam));
+ }
+ this.params = Collections.unmodifiableMap(paramsBuilder);
+ this.activatedByDefault = newRule.activatedByDefault();
+ this.deprecatedRuleKeys = Collections.unmodifiableSet(new TreeSet<>(newRule.deprecatedRuleKeys()));
+ }
+
+ public RulesDefinition.Repository repository() {
+ return repository;
+ }
+
+ @Override
+ @CheckForNull
+ public String pluginKey() {
+ return pluginKey;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public RuleScope scope() {
+ return scope;
+ }
+
+ @Override
+ public RuleType type() {
+ return type;
+ }
+
+ @Override
+ public String severity() {
+ return severity;
+ }
+
+ @Override
+ @CheckForNull
+ public String htmlDescription() {
+ return htmlDescription;
+ }
+
+ @Override
+ @CheckForNull
+ public String markdownDescription() {
+ return markdownDescription;
+ }
+
+ @Override
+ public boolean template() {
+ return template;
+ }
+
+ @Override
+ public boolean activatedByDefault() {
+ return activatedByDefault;
+ }
+
+ @Override
+ public RuleStatus status() {
+ return status;
+ }
+
+ @CheckForNull
+ @Deprecated
+ @Override
+ public String debtSubCharacteristic() {
+ return null;
+ }
+
+ @CheckForNull
+ @Override
+ public DebtRemediationFunction debtRemediationFunction() {
+ return debtRemediationFunction;
+ }
+
+ @Deprecated
+ @CheckForNull
+ @Override
+ public String effortToFixDescription() {
+ return gapDescription();
+ }
+
+ @CheckForNull
+ @Override
+ public String gapDescription() {
+ return gapDescription;
+ }
+
+ @CheckForNull
+ @Override
+ public RulesDefinition.Param param(String key) {
+ return params.get(key);
+ }
+
+ @Override
+ public List<RulesDefinition.Param> params() {
+ return unmodifiableList(new ArrayList<>(params.values()));
+ }
+
+ @Override
+ public Set<String> tags() {
+ return tags;
+ }
+
+ @Override
+ public Set<String> securityStandards() {
+ return securityStandards;
+ }
+
+ @Override
+ public Set<RuleKey> deprecatedRuleKeys() {
+ return deprecatedRuleKeys;
+ }
+
+ @CheckForNull
+ @Override
+ public String internalKey() {
+ return internalKey;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ DefaultRule other = (DefaultRule) o;
+ return key.equals(other.key) && repoKey.equals(other.repoKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = repoKey.hashCode();
+ result = 31 * result + key.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return format("[repository=%s, key=%s]", repoKey, key);
+ }
+}
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/RuleDefinitionContext.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/RuleDefinitionContext.java
new file mode 100644
index 00000000000..7d47fc25258
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/RuleDefinitionContext.java
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.server;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class RuleDefinitionContext extends RulesDefinition.Context {
+ private final Map<String, RulesDefinition.Repository> repositoriesByKey = new HashMap<>();
+ private String currentPluginKey;
+
+ @Override
+ public RulesDefinition.NewRepository createRepository(String key, String language) {
+ return new DefaultNewRepository(this, key, language, false);
+ }
+
+ @Override
+ public RulesDefinition.NewRepository createExternalRepository(String engineId, String language) {
+ return new DefaultNewRepository(this, RuleKey.EXTERNAL_RULE_REPO_PREFIX + engineId, language, true);
+ }
+
+ @Override
+ @Deprecated
+ public RulesDefinition.NewRepository extendRepository(String key, String language) {
+ return createRepository(key, language);
+ }
+
+ @Override
+ @CheckForNull
+ public RulesDefinition.Repository repository(String key) {
+ return repositoriesByKey.get(key);
+ }
+
+ @Override
+ public List<RulesDefinition.Repository> repositories() {
+ return unmodifiableList(new ArrayList<>(repositoriesByKey.values()));
+ }
+
+ @Override
+ @Deprecated
+ public List<RulesDefinition.ExtendedRepository> extendedRepositories(String repositoryKey) {
+ return emptyList();
+ }
+
+ @Override
+ @Deprecated
+ public List<RulesDefinition.ExtendedRepository> extendedRepositories() {
+ return emptyList();
+ }
+
+ void registerRepository(DefaultNewRepository newRepository) {
+ RulesDefinition.Repository existing = repositoriesByKey.get(newRepository.key());
+ if (existing != null) {
+ String existingLanguage = existing.language();
+ checkState(existingLanguage.equals(newRepository.language()),
+ "The rule repository '%s' must not be defined for two different languages: %s and %s",
+ newRepository.key(), existingLanguage, newRepository.language());
+ }
+ repositoriesByKey.put(newRepository.key(), new DefaultRepository(newRepository, existing));
+ }
+
+ public String currentPluginKey() {
+ return currentPluginKey;
+ }
+
+ @Override
+ public void setCurrentPluginKey(@Nullable String pluginKey) {
+ this.currentPluginKey = pluginKey;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/package-info.java
new file mode 100644
index 00000000000..42efaa610b6
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/server/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.server;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2.java
new file mode 100644
index 00000000000..ab25411b6b2
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/AlwaysIncreasingSystem2.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.utils;
+
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+import org.sonar.api.utils.System2;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+/**
+ * A subclass of {@link System2} which implementation of {@link System2#now()} always return a bigger value than the
+ * previous returned value.
+ * <p>
+ * This class is intended to be used in Unit tests.
+ * </p>
+ */
+public class AlwaysIncreasingSystem2 extends System2 {
+ private final AtomicLong now;
+ private final long increment;
+
+ private AlwaysIncreasingSystem2(Supplier<Long> initialValueSupplier, long increment) {
+ checkArgument(increment > 0, "increment must be > 0");
+ long initialValue = initialValueSupplier.get();
+ checkArgument(initialValue >= 0, "Initial value must be >= 0");
+ this.now = new AtomicLong(initialValue);
+ this.increment = increment;
+ }
+
+ public AlwaysIncreasingSystem2(long increment) {
+ this(AlwaysIncreasingSystem2::randomInitialValue, increment);
+ }
+
+ public AlwaysIncreasingSystem2(long initialValue, int increment) {
+ this(() -> initialValue, increment);
+ }
+
+ /**
+ * Values returned by {@link #now()} will start with a random value and increment by 100.
+ */
+ public AlwaysIncreasingSystem2() {
+ this(AlwaysIncreasingSystem2::randomInitialValue, 100);
+ }
+
+ @Override
+ public long now() {
+ return now.getAndAdd(increment);
+ }
+
+ private static long randomInitialValue() {
+ return (long) Math.abs(new Random().nextInt(2_000_000));
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/DefaultTempFolder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/DefaultTempFolder.java
new file mode 100644
index 00000000000..369058f7b40
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/DefaultTempFolder.java
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.utils;
+
+import java.nio.file.FileVisitResult;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import org.apache.commons.io.FileUtils;
+import org.sonar.api.utils.TempFolder;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+public class DefaultTempFolder implements TempFolder {
+ private static final Logger LOG = Loggers.get(DefaultTempFolder.class);
+
+ private final File tempDir;
+ private final boolean deleteOnExit;
+
+ public DefaultTempFolder(File tempDir) {
+ this(tempDir, false);
+ }
+
+ public DefaultTempFolder(File tempDir, boolean deleteOnExit) {
+ this.tempDir = tempDir;
+ this.deleteOnExit = deleteOnExit;
+ }
+
+ @Override
+ public File newDir() {
+ return createTempDir(tempDir.toPath()).toFile();
+ }
+
+ private static Path createTempDir(Path baseDir) {
+ try {
+ return Files.createTempDirectory(baseDir, null);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create temp directory", e);
+ }
+ }
+
+ @Override
+ public File newDir(String name) {
+ File dir = new File(tempDir, name);
+ try {
+ FileUtils.forceMkdir(dir);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create temp directory - " + dir, e);
+ }
+ return dir;
+ }
+
+ @Override
+ public File newFile() {
+ return newFile(null, null);
+ }
+
+ @Override
+ public File newFile(@Nullable String prefix, @Nullable String suffix) {
+ return createTempFile(tempDir.toPath(), prefix, suffix).toFile();
+ }
+
+ private static Path createTempFile(Path baseDir, String prefix, String suffix) {
+ try {
+ return Files.createTempFile(baseDir, prefix, suffix);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create temp file", e);
+ }
+ }
+
+ public void clean() {
+ try {
+ if (tempDir.exists()) {
+ Files.walkFileTree(tempDir.toPath(), DeleteRecursivelyFileVisitor.INSTANCE);
+ }
+ } catch (IOException e) {
+ LOG.error("Failed to delete temp folder", e);
+ }
+ }
+
+ public void stop() {
+ if (deleteOnExit) {
+ clean();
+ }
+ }
+
+ private static final class DeleteRecursivelyFileVisitor extends SimpleFileVisitor<Path> {
+ public static final DeleteRecursivelyFileVisitor INSTANCE = new DeleteRecursivelyFileVisitor();
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.deleteIfExists(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ Files.deleteIfExists(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/JUnitTempFolder.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/JUnitTempFolder.java
new file mode 100644
index 00000000000..a52da93ca7c
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/JUnitTempFolder.java
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.utils;
+
+import org.apache.commons.lang.StringUtils;
+import org.junit.rules.ExternalResource;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.sonar.api.utils.TempFolder;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Implementation of {@link org.sonar.api.utils.TempFolder} to be used
+ * only in JUnit tests. It wraps {@link org.junit.rules.TemporaryFolder}.
+ * <br>
+ * Example:
+ * <pre>
+ * public class MyTest {
+ * &#064;@org.junit.Rule
+ * public JUnitTempFolder temp = new JUnitTempFolder();
+ *
+ * &#064;@org.junit.Test
+ * public void myTest() throws Exception {
+ * File dir = temp.newDir();
+ * // ...
+ * }
+ * }
+ * </pre>
+ *
+ * @since 5.1
+ */
+public class JUnitTempFolder extends ExternalResource implements TempFolder {
+
+ private final TemporaryFolder junit = new TemporaryFolder();
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return junit.apply(base, description);
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ junit.create();
+ }
+
+ @Override
+ protected void after() {
+ junit.delete();
+ }
+
+ @Override
+ public File newDir() {
+ try {
+ return junit.newFolder();
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create temp dir", e);
+ }
+ }
+
+ @Override
+ public File newDir(String name) {
+ try {
+ return junit.newFolder(name);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create temp dir", e);
+ }
+ }
+
+ @Override
+ public File newFile() {
+ try {
+ return junit.newFile();
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create temp file", e);
+ }
+ }
+
+ @Override
+ public File newFile(@Nullable String prefix, @Nullable String suffix) {
+ try {
+ return junit.newFile(StringUtils.defaultString(prefix) + "-" + StringUtils.defaultString(suffix));
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create temp file", e);
+ }
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/ScannerUtils.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/ScannerUtils.java
new file mode 100644
index 00000000000..f23bfbac928
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/ScannerUtils.java
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.utils;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+
+public class ScannerUtils {
+
+ private ScannerUtils() {
+ }
+
+ /**
+ * 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(url == null ? "" : url, StandardCharsets.UTF_8.name());
+
+ } 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) {
+ String str = o.toString();
+ if (str != null) {
+ return str;
+ }
+ }
+ } catch (Exception e) {
+ // fallback
+ }
+
+ return o.getClass().getName();
+ }
+
+ public static String pluralize(String str, int i) {
+ if (i == 1) {
+ return str;
+ }
+ return str + "s";
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java
new file mode 100644
index 00000000000..b1005d5e186
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.utils;
+
+import java.util.TimeZone;
+import org.sonar.api.utils.System2;
+
+public class TestSystem2 extends System2 {
+
+ private long now = 0L;
+ private TimeZone defaultTimeZone = getDefaultTimeZone();
+
+ public TestSystem2 setNow(long l) {
+ this.now = l;
+ return this;
+ }
+
+ @Override
+ public long now() {
+ if (now <= 0L) {
+ throw new IllegalStateException("Method setNow() was not called by test");
+ }
+ return now;
+ }
+
+ public TestSystem2 setDefaultTimeZone(TimeZone defaultTimeZone) {
+ this.defaultTimeZone = defaultTimeZone;
+ return this;
+ }
+
+ @Override
+ public TimeZone getDefaultTimeZone() {
+ return defaultTimeZone;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/WorkDuration.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/WorkDuration.java
new file mode 100644
index 00000000000..20349ba155c
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/WorkDuration.java
@@ -0,0 +1,194 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.utils;
+
+import java.io.Serializable;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+
+/**
+ * @since 4.2
+ */
+public class WorkDuration implements Serializable {
+
+ static final int DAY_POSITION_IN_LONG = 10_000;
+ static final int HOUR_POSITION_IN_LONG = 100;
+ static final int MINUTE_POSITION_IN_LONG = 1;
+
+ public enum UNIT {
+ DAYS, HOURS, MINUTES
+ }
+
+ private int hoursInDay;
+
+ private long durationInMinutes;
+ private int days;
+ private int hours;
+ private int minutes;
+
+ private WorkDuration(long durationInMinutes, int days, int hours, int minutes, int hoursInDay) {
+ this.durationInMinutes = durationInMinutes;
+ this.days = days;
+ this.hours = hours;
+ this.minutes = minutes;
+ this.hoursInDay = hoursInDay;
+ }
+
+ public static WorkDuration create(int days, int hours, int minutes, int hoursInDay) {
+ long durationInSeconds = 60L * days * hoursInDay;
+ durationInSeconds += 60L * hours;
+ durationInSeconds += minutes;
+ return new WorkDuration(durationInSeconds, days, hours, minutes, hoursInDay);
+ }
+
+ public static WorkDuration createFromValueAndUnit(int value, UNIT unit, int hoursInDay) {
+ switch (unit) {
+ case DAYS:
+ return create(value, 0, 0, hoursInDay);
+ case HOURS:
+ return create(0, value, 0, hoursInDay);
+ case MINUTES:
+ return create(0, 0, value, hoursInDay);
+ default:
+ throw new IllegalStateException("Cannot create work duration");
+ }
+ }
+
+ static WorkDuration createFromLong(long duration, int hoursInDay) {
+ int days = 0;
+ int hours = 0;
+ int minutes = 0;
+
+ long time = duration;
+ Long currentTime = time / WorkDuration.DAY_POSITION_IN_LONG;
+ if (currentTime > 0) {
+ days = currentTime.intValue();
+ time = time - (currentTime * WorkDuration.DAY_POSITION_IN_LONG);
+ }
+
+ currentTime = time / WorkDuration.HOUR_POSITION_IN_LONG;
+ if (currentTime > 0) {
+ hours = currentTime.intValue();
+ time = time - (currentTime * WorkDuration.HOUR_POSITION_IN_LONG);
+ }
+
+ currentTime = time / WorkDuration.MINUTE_POSITION_IN_LONG;
+ if (currentTime > 0) {
+ minutes = currentTime.intValue();
+ }
+ return WorkDuration.create(days, hours, minutes, hoursInDay);
+ }
+
+ static WorkDuration createFromMinutes(long duration, int hoursInDay) {
+ int days = (int)(duration / (double)hoursInDay / 60.0);
+ Long currentDurationInMinutes = duration - (60L * days * hoursInDay);
+ int hours = (int)(currentDurationInMinutes / 60.0);
+ currentDurationInMinutes = currentDurationInMinutes - (60L * hours);
+ return new WorkDuration(duration, days, hours, currentDurationInMinutes.intValue(), hoursInDay);
+ }
+
+ /**
+ * Return the duration in number of working days.
+ * For instance, 3 days and 4 hours will return 3.5 days (if hoursIndDay is 8).
+ */
+ public double toWorkingDays() {
+ return durationInMinutes / 60d / hoursInDay;
+ }
+
+ /**
+ * Return the duration using the following format DDHHMM, where DD is the number of days, HH is the number of months, and MM the number of minutes.
+ * For instance, 3 days and 4 hours will return 030400 (if hoursIndDay is 8).
+ */
+ public long toLong() {
+ int workingDays = days;
+ int workingHours = hours;
+ if (hours >= hoursInDay) {
+ int nbAdditionalDays = hours / hoursInDay;
+ workingDays += nbAdditionalDays;
+ workingHours = hours - (nbAdditionalDays * hoursInDay);
+ }
+ return 1L * workingDays * DAY_POSITION_IN_LONG + workingHours * HOUR_POSITION_IN_LONG + minutes * MINUTE_POSITION_IN_LONG;
+ }
+
+ public long toMinutes() {
+ return durationInMinutes;
+ }
+
+ public WorkDuration add(@Nullable WorkDuration with) {
+ if (with != null) {
+ return WorkDuration.createFromMinutes(this.toMinutes() + with.toMinutes(), this.hoursInDay);
+ } else {
+ return this;
+ }
+ }
+
+ public WorkDuration subtract(@Nullable WorkDuration with) {
+ if (with != null) {
+ return WorkDuration.createFromMinutes(this.toMinutes() - with.toMinutes(), this.hoursInDay);
+ } else {
+ return this;
+ }
+ }
+
+ public WorkDuration multiply(int factor) {
+ return WorkDuration.createFromMinutes(this.toMinutes() * factor, this.hoursInDay);
+ }
+
+ public int days() {
+ return days;
+ }
+
+ public int hours() {
+ return hours;
+ }
+
+ public int minutes() {
+ return minutes;
+ }
+
+ int hoursInDay() {
+ return hoursInDay;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ WorkDuration that = (WorkDuration) o;
+ return durationInMinutes == that.durationInMinutes;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (durationInMinutes ^ (durationInMinutes >>> 32));
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/package-info.java
new file mode 100644
index 00000000000..335c370e59a
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.utils;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/PartImpl.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/PartImpl.java
new file mode 100644
index 00000000000..ed161265484
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/PartImpl.java
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.ws;
+
+import java.io.InputStream;
+import org.sonar.api.server.ws.Request;
+
+public class PartImpl implements Request.Part {
+
+ private final InputStream inputStream;
+ private final String fileName;
+
+ public PartImpl(InputStream inputStream, String fileName) {
+ this.inputStream = inputStream;
+ this.fileName = fileName;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ @Override
+ public String getFileName() {
+ return fileName;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/SimpleGetRequest.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/SimpleGetRequest.java
new file mode 100644
index 00000000000..a1135669d53
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/SimpleGetRequest.java
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.ws;
+
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.server.ws.LocalConnector;
+import org.sonar.api.server.ws.Request;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Fake implementation of {@link org.sonar.api.server.ws.Request} used
+ * for testing. Call the method {@link #setParam(String, String)} to
+ * emulate some parameter values.
+ */
+public class SimpleGetRequest extends Request {
+
+ private final Map<String, String[]> params = new HashMap<>();
+ private final Map<String, Part> parts = new HashMap<>();
+ private final Map<String, String> headers = new HashMap<>();
+ private String mediaType = "application/json";
+ private String path;
+
+ @Override
+ public String method() {
+ return "GET";
+ }
+
+ @Override
+ public String getMediaType() {
+ return mediaType;
+ }
+
+ public SimpleGetRequest setMediaType(String mediaType) {
+ requireNonNull(mediaType);
+ this.mediaType = mediaType;
+ return this;
+ }
+
+ @Override
+ public boolean hasParam(String key) {
+ return params.keySet().contains(key);
+ }
+
+ @Override
+ public String param(String key) {
+ String[] strings = params.get(key);
+ return strings == null || strings.length == 0 ? null : strings[0];
+ }
+
+ @Override
+ public List<String> multiParam(String key) {
+ String value = param(key);
+ return value == null ? emptyList() : singletonList(value);
+ }
+
+ @Override
+ @CheckForNull
+ public List<String> paramAsStrings(String key) {
+ String value = param(key);
+ if (value == null) {
+ return null;
+ }
+
+ return Arrays.stream(value.split(",")).map(String::trim).filter(x -> !x.isEmpty()).collect(Collectors.toList());
+ }
+
+ @Override
+ public InputStream paramAsInputStream(String key) {
+ return IOUtils.toInputStream(param(key), UTF_8);
+ }
+
+ public SimpleGetRequest setParam(String key, @Nullable String value) {
+ if (value != null) {
+ params.put(key, new String[] {value});
+ }
+ return this;
+ }
+
+ @Override
+ public Map<String, String[]> getParams() {
+ return params;
+ }
+
+ @Override
+ public Part paramAsPart(String key) {
+ return parts.get(key);
+ }
+
+ public SimpleGetRequest setPart(String key, InputStream input, String fileName) {
+ parts.put(key, new PartImpl(input, fileName));
+ return this;
+ }
+
+ @Override
+ public LocalConnector localConnector() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ public SimpleGetRequest setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ @Override
+ public Optional<String> header(String name) {
+ return Optional.ofNullable(headers.get(name));
+ }
+
+ public SimpleGetRequest setHeader(String name, String value) {
+ headers.put(name, value);
+ return this;
+ }
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/ValidatingRequest.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/ValidatingRequest.java
new file mode 100644
index 00000000000..6e7d90b6a06
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/ValidatingRequest.java
@@ -0,0 +1,242 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.api.impl.ws;
+
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.server.ws.LocalConnector;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.WebService;
+
+import static java.lang.String.format;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+import static org.apache.commons.lang.StringUtils.defaultString;
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+/**
+ * @since 4.2
+ */
+public abstract class ValidatingRequest extends Request {
+
+ private static final String COMMA_SPLITTER = ",";
+ private WebService.Action action;
+ private LocalConnector localConnector;
+
+ public void setAction(WebService.Action action) {
+ this.action = action;
+ }
+
+ public WebService.Action action() {
+ return action;
+ }
+
+ @Override
+ public LocalConnector localConnector() {
+ requireNonNull(localConnector, "Local connector has not been set");
+ return localConnector;
+ }
+
+ public void setLocalConnector(LocalConnector lc) {
+ this.localConnector = lc;
+ }
+
+ @Override
+ @CheckForNull
+ public String param(String key) {
+ WebService.Param definition = action.param(key);
+ String rawValue = readParam(key, definition);
+ String rawValueOrDefault = defaultString(rawValue, definition.defaultValue());
+ String value = rawValueOrDefault == null ? null : trim(rawValueOrDefault);
+ validateRequiredValue(key, definition, rawValue);
+ if (value == null) {
+ return null;
+ }
+ validatePossibleValues(key, value, definition);
+ validateMaximumLength(key, definition, rawValueOrDefault);
+ validateMinimumLength(key, definition, rawValueOrDefault);
+ validateMaximumValue(key, definition, value);
+ return value;
+ }
+
+ @Override
+ public List<String> multiParam(String key) {
+ WebService.Param definition = action.param(key);
+ List<String> values = readMultiParamOrDefaultValue(key, definition);
+ return validateValues(values, definition);
+ }
+
+ private static String trim(String s) {
+ int begin;
+ for (begin = 0; begin < s.length(); begin++) {
+ if (!Character.isWhitespace(s.charAt(begin))) {
+ break;
+ }
+ }
+
+ int end;
+ for (end = s.length(); end > begin; end--) {
+ if (!Character.isWhitespace(s.charAt(end - 1))) {
+ break;
+ }
+ }
+ return s.substring(begin, end);
+ }
+
+ @Override
+ @CheckForNull
+ public InputStream paramAsInputStream(String key) {
+ return readInputStreamParam(key);
+ }
+
+ @Override
+ @CheckForNull
+ public Part paramAsPart(String key) {
+ return readPart(key);
+ }
+
+ @CheckForNull
+ @Override
+ public List<String> paramAsStrings(String key) {
+ WebService.Param definition = action.param(key);
+ String value = defaultString(readParam(key, definition), definition.defaultValue());
+ if (value == null) {
+ return null;
+ }
+ List<String> values = Arrays.stream(value.split(COMMA_SPLITTER))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toList());
+ return validateValues(values, definition);
+ }
+
+ @CheckForNull
+ @Override
+ public <E extends Enum<E>> List<E> paramAsEnums(String key, Class<E> enumClass) {
+ List<String> values = paramAsStrings(key);
+ if (values == null) {
+ return null;
+ }
+ return values.stream()
+ .filter(s -> !s.isEmpty())
+ .map(value -> Enum.valueOf(enumClass, value))
+ .collect(Collectors.toList());
+ }
+
+ @CheckForNull
+ private String readParam(String key, @Nullable WebService.Param definition) {
+ checkArgument(definition != null, "BUG - parameter '%s' is undefined for action '%s'", key, action.key());
+ String deprecatedKey = definition.deprecatedKey();
+ return deprecatedKey != null ? defaultString(readParam(deprecatedKey), readParam(key)) : readParam(key);
+ }
+
+ private List<String> readMultiParamOrDefaultValue(String key, @Nullable WebService.Param definition) {
+ checkArgument(definition != null, "BUG - parameter '%s' is undefined for action '%s'", key, action.key());
+
+ List<String> keyValues = readMultiParam(key);
+ if (!keyValues.isEmpty()) {
+ return keyValues;
+ }
+
+ String deprecatedKey = definition.deprecatedKey();
+ List<String> deprecatedKeyValues = deprecatedKey == null ? emptyList() : readMultiParam(deprecatedKey);
+ if (!deprecatedKeyValues.isEmpty()) {
+ return deprecatedKeyValues;
+ }
+
+ String defaultValue = definition.defaultValue();
+ return defaultValue == null ? emptyList() : singletonList(defaultValue);
+ }
+
+ @CheckForNull
+ protected abstract String readParam(String key);
+
+ protected abstract List<String> readMultiParam(String key);
+
+ @CheckForNull
+ protected abstract InputStream readInputStreamParam(String key);
+
+ @CheckForNull
+ protected abstract Part readPart(String key);
+
+ private static List<String> validateValues(List<String> values, WebService.Param definition) {
+ Integer maximumValues = definition.maxValuesAllowed();
+ checkArgument(maximumValues == null || values.size() <= maximumValues, "'%s' can contains only %s values, got %s", definition.key(), maximumValues, values.size());
+ values.forEach(value -> validatePossibleValues(definition.key(), value, definition));
+ return values;
+ }
+
+ private static void validatePossibleValues(String key, String value, WebService.Param definition) {
+ Set<String> possibleValues = definition.possibleValues();
+ if (possibleValues == null) {
+ return;
+ }
+ checkArgument(possibleValues.contains(value), "Value of parameter '%s' (%s) must be one of: %s", key, value, possibleValues);
+ }
+
+ private static void validateMaximumLength(String key, WebService.Param definition, String valueOrDefault) {
+ Integer maximumLength = definition.maximumLength();
+ if (maximumLength == null) {
+ return;
+ }
+ int valueLength = valueOrDefault.length();
+ checkArgument(valueLength <= maximumLength, "'%s' length (%s) is longer than the maximum authorized (%s)", key, valueLength, maximumLength);
+ }
+
+ private static void validateMinimumLength(String key, WebService.Param definition, String valueOrDefault) {
+ Integer minimumLength = definition.minimumLength();
+ if (minimumLength == null) {
+ return;
+ }
+ int valueLength = valueOrDefault.length();
+ checkArgument(valueLength >= minimumLength, "'%s' length (%s) is shorter than the minimum authorized (%s)", key, valueLength, minimumLength);
+ }
+
+ private static void validateMaximumValue(String key, WebService.Param definition, String value) {
+ Integer maximumValue = definition.maximumValue();
+ if (maximumValue == null) {
+ return;
+ }
+ int valueAsInt = validateAsNumeric(key, value);
+ checkArgument(valueAsInt <= maximumValue, "'%s' value (%s) must be less than %s", key, valueAsInt, maximumValue);
+ }
+
+ private static void validateRequiredValue(String key, WebService.Param definition, String value) {
+ boolean required = definition.isRequired();
+ if (required) {
+ checkArgument(value != null, format(MSG_PARAMETER_MISSING, key));
+ }
+ }
+
+ private static int validateAsNumeric(String key, String value) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException exception) {
+ throw new IllegalArgumentException(format("'%s' value '%s' cannot be parsed as an integer", key, value), exception);
+ }
+ }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/package-info.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/package-info.java
new file mode 100644
index 00000000000..306c7a7c165
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/ws/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.api.impl.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;