3 * Copyright (C) 2009-2022 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.qualityprofile.builtin;
22 import com.google.common.collect.ImmutableList;
23 import com.google.common.collect.Multimap;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.LinkedHashMap;
29 import java.util.List;
31 import java.util.Optional;
33 import java.util.stream.Collectors;
34 import javax.annotation.Nullable;
35 import org.sonar.api.profiles.RulesProfile;
36 import org.sonar.api.resources.Language;
37 import org.sonar.api.resources.Languages;
38 import org.sonar.api.rule.RuleKey;
39 import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
40 import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition.BuiltInQualityProfile;
41 import org.sonar.api.utils.log.Logger;
42 import org.sonar.api.utils.log.Loggers;
43 import org.sonar.api.utils.log.Profiler;
44 import org.sonar.core.util.stream.MoreCollectors;
45 import org.sonar.db.DbClient;
46 import org.sonar.db.DbSession;
47 import org.sonar.db.rule.DeprecatedRuleKeyDto;
48 import org.sonar.db.rule.RuleDefinitionDto;
49 import org.sonar.server.rule.ServerRuleFinder;
50 import org.springframework.beans.factory.annotation.Autowired;
52 import static com.google.common.base.Preconditions.checkState;
54 public class BuiltInQProfileRepositoryImpl implements BuiltInQProfileRepository {
55 private static final Logger LOGGER = Loggers.get(BuiltInQProfileRepositoryImpl.class);
56 private static final String DEFAULT_PROFILE_NAME = "Sonar way";
58 private final DbClient dbClient;
59 private final ServerRuleFinder ruleFinder;
60 private final Languages languages;
61 private final List<BuiltInQualityProfilesDefinition> definitions;
62 private List<BuiltInQProfile> qProfiles;
65 * Used by the ioc container when no {@link BuiltInQualityProfilesDefinition} is defined at all
67 @Autowired(required = false)
68 public BuiltInQProfileRepositoryImpl(DbClient dbClient, ServerRuleFinder ruleFinder, Languages languages) {
69 this(dbClient, ruleFinder, languages, new BuiltInQualityProfilesDefinition[0]);
72 @Autowired(required = false)
73 public BuiltInQProfileRepositoryImpl(DbClient dbClient, ServerRuleFinder ruleFinder, Languages languages, BuiltInQualityProfilesDefinition... definitions) {
74 this.dbClient = dbClient;
75 this.ruleFinder = ruleFinder;
76 this.languages = languages;
77 this.definitions = ImmutableList.copyOf(definitions);
81 public void initialize() {
82 checkState(qProfiles == null, "initialize must be called only once");
84 Profiler profiler = Profiler.create(LOGGER).startInfo("Load quality profiles");
85 BuiltInQualityProfilesDefinition.Context context = new BuiltInQualityProfilesDefinition.Context();
86 for (BuiltInQualityProfilesDefinition definition : definitions) {
87 definition.define(context);
89 Map<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguage = validateAndClean(context);
90 this.qProfiles = toFlatList(rulesProfilesByLanguage);
91 ensureAllLanguagesHaveAtLeastOneBuiltInQP();
96 public List<BuiltInQProfile> get() {
97 checkState(qProfiles != null, "initialize must be called first");
102 private void ensureAllLanguagesHaveAtLeastOneBuiltInQP() {
103 Set<String> languagesWithBuiltInQProfiles = qProfiles.stream().map(BuiltInQProfile::getLanguage).collect(Collectors.toSet());
104 Set<String> languagesWithoutBuiltInQProfiles = Arrays.stream(languages.all())
105 .map(Language::getKey)
106 .filter(key -> !languagesWithBuiltInQProfiles.contains(key))
107 .collect(Collectors.toSet());
109 checkState(languagesWithoutBuiltInQProfiles.isEmpty(), "The following languages have no built-in quality profiles: %s",
110 String.join("", languagesWithoutBuiltInQProfiles));
113 private Map<String, Map<String, BuiltInQualityProfile>> validateAndClean(BuiltInQualityProfilesDefinition.Context context) {
114 Map<String, Map<String, BuiltInQualityProfile>> profilesByLanguageAndName = context.profilesByLanguageAndName();
115 profilesByLanguageAndName.entrySet()
117 String language = entry.getKey();
118 if (languages.get(language) == null) {
119 LOGGER.info("Language {} is not installed, related quality profiles are ignored", language);
125 return profilesByLanguageAndName;
128 private List<BuiltInQProfile> toFlatList(Map<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguage) {
129 if (rulesProfilesByLanguage.isEmpty()) {
130 return Collections.emptyList();
132 Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = loadRuleDefinitionsByRuleKey();
133 Map<String, List<BuiltInQProfile.Builder>> buildersByLanguage = rulesProfilesByLanguage
136 .collect(MoreCollectors.uniqueIndex(
138 rulesProfilesByLanguageAndName -> toQualityProfileBuilders(rulesProfilesByLanguageAndName, rulesByRuleKey)));
139 return buildersByLanguage
142 .filter(BuiltInQProfileRepositoryImpl::ensureAtMostOneDeclaredDefault)
143 .map(entry -> toQualityProfiles(entry.getValue()))
144 .flatMap(Collection::stream)
145 .collect(MoreCollectors.toList());
148 private Map<RuleKey, RuleDefinitionDto> loadRuleDefinitionsByRuleKey() {
149 try (DbSession dbSession = dbClient.openSession(false)) {
150 Collection<RuleDefinitionDto> ruleDefinitions = ruleFinder.findAll();
151 Multimap<String, DeprecatedRuleKeyDto> deprecatedRuleKeysByRuleId = dbClient.ruleDao().selectAllDeprecatedRuleKeys(dbSession).stream()
152 .collect(MoreCollectors.index(DeprecatedRuleKeyDto::getRuleUuid));
153 Map<RuleKey, RuleDefinitionDto> rulesByRuleKey = new HashMap<>();
154 for (RuleDefinitionDto ruleDefinition : ruleDefinitions) {
155 rulesByRuleKey.put(ruleDefinition.getKey(), ruleDefinition);
156 deprecatedRuleKeysByRuleId.get(ruleDefinition.getUuid()).forEach(t -> rulesByRuleKey.put(RuleKey.of(t.getOldRepositoryKey(), t.getOldRuleKey()), ruleDefinition));
158 return rulesByRuleKey;
163 * Creates {@link BuiltInQProfile.Builder} for each unique quality profile name for a given language.
164 * Builders will have the following properties populated:
166 * <li>{@link BuiltInQProfile.Builder#language language}: key of the method's parameter</li>
167 * <li>{@link BuiltInQProfile.Builder#name name}: {@link RulesProfile#getName()}</li>
168 * <li>{@link BuiltInQProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile
169 * with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}</li>
170 * <li>{@link BuiltInQProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all
171 * RulesProfile with a given name</li>
174 private static List<BuiltInQProfile.Builder> toQualityProfileBuilders(Map.Entry<String, Map<String, BuiltInQualityProfile>> rulesProfilesByLanguageAndName,
175 Map<RuleKey, RuleDefinitionDto> rulesByRuleKey) {
176 String language = rulesProfilesByLanguageAndName.getKey();
177 // use a LinkedHashMap to keep order of insertion of RulesProfiles
178 Map<String, BuiltInQProfile.Builder> qualityProfileBuildersByName = new LinkedHashMap<>();
179 for (BuiltInQualityProfile builtInProfile : rulesProfilesByLanguageAndName.getValue().values()) {
180 qualityProfileBuildersByName.compute(
181 builtInProfile.name(),
182 (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, builtInProfile, rulesByRuleKey));
184 return ImmutableList.copyOf(qualityProfileBuildersByName.values());
188 * Fails if more than one {@link BuiltInQProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}.
190 private static boolean ensureAtMostOneDeclaredDefault(Map.Entry<String, List<BuiltInQProfile.Builder>> entry) {
191 Set<String> declaredDefaultProfileNames = entry.getValue().stream()
192 .filter(BuiltInQProfile.Builder::isDeclaredDefault)
193 .map(BuiltInQProfile.Builder::getName)
194 .collect(MoreCollectors.toSet());
195 checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames);
199 private static BuiltInQProfile.Builder updateOrCreateBuilder(String language, @Nullable BuiltInQProfile.Builder existingBuilder, BuiltInQualityProfile builtInProfile,
200 Map<RuleKey, RuleDefinitionDto> rulesByRuleKey) {
201 BuiltInQProfile.Builder builder = createOrReuseBuilder(existingBuilder, language, builtInProfile);
202 builder.setDeclaredDefault(builtInProfile.isDefault());
203 builtInProfile.rules().forEach(builtInActiveRule -> {
204 RuleKey ruleKey = RuleKey.of(builtInActiveRule.repoKey(), builtInActiveRule.ruleKey());
205 RuleDefinitionDto ruleDefinition = rulesByRuleKey.get(ruleKey);
206 checkState(ruleDefinition != null, "Rule with key '%s' not found", ruleKey);
207 builder.addRule(new BuiltInQProfile.ActiveRule(ruleDefinition.getUuid(), ruleDefinition.getKey(),
208 builtInActiveRule.overriddenSeverity(), builtInActiveRule.overriddenParams()));
213 private static BuiltInQProfile.Builder createOrReuseBuilder(@Nullable BuiltInQProfile.Builder existingBuilder, String language, BuiltInQualityProfile builtInProfile) {
214 if (existingBuilder == null) {
215 return new BuiltInQProfile.Builder()
216 .setLanguage(language)
217 .setName(builtInProfile.name());
219 return existingBuilder;
222 private static List<BuiltInQProfile> toQualityProfiles(List<BuiltInQProfile.Builder> builders) {
223 if (builders.stream().noneMatch(BuiltInQProfile.Builder::isDeclaredDefault)) {
224 Optional<BuiltInQProfile.Builder> sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst();
225 if (sonarWayProfile.isPresent()) {
226 sonarWayProfile.get().setComputedDefault(true);
228 builders.iterator().next().setComputedDefault(true);
231 return builders.stream()
232 .map(BuiltInQProfile.Builder::build)
233 .collect(MoreCollectors.toList(builders.size()));