diff options
author | Duarte Meneses <duarte.meneses@sonarsource.com> | 2019-06-19 13:56:51 -0500 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-07-12 20:21:14 +0200 |
commit | 93dc9770902dc7e168869d88b5ad731bfc0bedd9 (patch) | |
tree | 97ba885661d5cd9a2115fe212df31bacec9f9947 /sonar-plugin-api-impl/src/main | |
parent | 7c7d9b6b90244d2c974207862071caccdb2c9bb5 (diff) | |
download | sonarqube-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')
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> + * <sonar.exclusions> + * src/foo, + * src/bar, + * src/biz + * <sonar.exclusions> + * </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> + * <sonar.exclusions> + * a + * b, + * c + * <sonar.exclusions> + * </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 { + * @@org.junit.Rule + * public JUnitTempFolder temp = new JUnitTempFolder(); + * + * @@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; |