3 * Copyright (C) 2009-2024 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;
22 import com.google.common.collect.ArrayListMultimap;
23 import com.google.common.collect.Multimap;
24 import java.time.Instant;
25 import java.time.ZoneId;
26 import java.time.format.DateTimeFormatter;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Locale;
34 import java.util.function.Function;
35 import org.apache.commons.lang.StringUtils;
36 import org.sonar.api.Startable;
37 import org.sonar.api.resources.Language;
38 import org.sonar.api.resources.Languages;
39 import org.sonar.api.server.ServerSide;
40 import org.sonar.api.utils.System2;
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.db.DbClient;
45 import org.sonar.db.DbSession;
46 import org.sonar.db.qualityprofile.DefaultQProfileDto;
47 import org.sonar.db.qualityprofile.QProfileDto;
48 import org.sonar.db.qualityprofile.RulesProfileDto;
49 import org.sonar.server.qualityprofile.builtin.BuiltInQProfile;
50 import org.sonar.server.qualityprofile.builtin.BuiltInQProfileInsert;
51 import org.sonar.server.qualityprofile.builtin.BuiltInQProfileRepository;
52 import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdate;
53 import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListener;
54 import org.sonar.server.qualityprofile.builtin.QProfileName;
56 import static java.lang.String.format;
57 import static java.util.stream.Collectors.toMap;
58 import static java.util.stream.Collectors.toSet;
59 import static org.sonar.server.qualityprofile.ActiveRuleInheritance.NONE;
62 * Synchronize Quality profiles during server startup
65 public class RegisterQualityProfiles implements Startable {
67 private static final Logger LOGGER = Loggers.get(RegisterQualityProfiles.class);
69 private final BuiltInQProfileRepository builtInQProfileRepository;
70 private final DbClient dbClient;
71 private final BuiltInQProfileInsert builtInQProfileInsert;
72 private final BuiltInQProfileUpdate builtInQProfileUpdate;
73 private final BuiltInQualityProfilesUpdateListener builtInQualityProfilesNotification;
74 private final System2 system2;
75 private final Languages languages;
77 public RegisterQualityProfiles(BuiltInQProfileRepository builtInQProfileRepository,
78 DbClient dbClient, BuiltInQProfileInsert builtInQProfileInsert, BuiltInQProfileUpdate builtInQProfileUpdate,
79 BuiltInQualityProfilesUpdateListener builtInQualityProfilesNotification, System2 system2, Languages languages) {
80 this.builtInQProfileRepository = builtInQProfileRepository;
81 this.dbClient = dbClient;
82 this.builtInQProfileInsert = builtInQProfileInsert;
83 this.builtInQProfileUpdate = builtInQProfileUpdate;
84 this.builtInQualityProfilesNotification = builtInQualityProfilesNotification;
85 this.system2 = system2;
86 this.languages = languages;
91 List<BuiltInQProfile> builtInQProfiles = builtInQProfileRepository.get();
92 if (builtInQProfiles.isEmpty()) {
96 Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Register quality profiles");
97 try (DbSession dbSession = dbClient.openSession(false);
98 DbSession batchDbSession = dbClient.openSession(true)) {
99 long startDate = system2.now();
101 Map<QProfileName, RulesProfileDto> persistedRuleProfiles = loadPersistedProfiles(dbSession);
103 Multimap<QProfileName, ActiveRuleChange> changedProfiles = ArrayListMultimap.create();
104 builtInQProfiles.forEach(builtIn -> {
105 RulesProfileDto ruleProfile = persistedRuleProfiles.get(builtIn.getQProfileName());
106 if (ruleProfile == null) {
107 create(dbSession, batchDbSession, builtIn);
109 List<ActiveRuleChange> changes = update(dbSession, builtIn, ruleProfile);
110 changedProfiles.putAll(builtIn.getQProfileName(), changes.stream()
112 String inheritance = change.getActiveRule().getInheritance();
113 return inheritance == null || NONE.name().equals(inheritance);
118 if (!changedProfiles.isEmpty()) {
119 long endDate = system2.now();
120 builtInQualityProfilesNotification.onChange(changedProfiles, startDate, endDate);
122 ensureBuiltInDefaultQPContainsRules(dbSession);
123 unsetBuiltInFlagAndRenameQPWhenPluginUninstalled(dbSession);
124 ensureBuiltInAreDefaultQPWhenNoRules(dbSession);
128 profiler.stopDebug();
136 private Map<QProfileName, RulesProfileDto> loadPersistedProfiles(DbSession dbSession) {
137 return dbClient.qualityProfileDao().selectBuiltInRuleProfiles(dbSession).stream()
138 .collect(toMap(rp -> new QProfileName(rp.getLanguage(), rp.getName()), Function.identity()));
141 private void create(DbSession dbSession, DbSession batchDbSession, BuiltInQProfile builtIn) {
142 LOGGER.info("Register profile {}", builtIn.getQProfileName());
144 renameOutdatedProfiles(dbSession, builtIn);
146 builtInQProfileInsert.create(batchDbSession, builtIn);
149 private List<ActiveRuleChange> update(DbSession dbSession, BuiltInQProfile definition, RulesProfileDto dbProfile) {
150 LOGGER.info("Update profile {}", definition.getQProfileName());
152 return builtInQProfileUpdate.update(dbSession, definition, dbProfile);
156 * The Quality profiles created by users should be renamed when they have the same name
157 * as the built-in profile to be persisted.
159 * When upgrading from < 6.5 , all existing profiles are considered as "custom" (created
160 * by users) because the concept of built-in profile is not persisted. The "Sonar way" profiles
161 * are renamed to "Sonar way (outdated copy) in order to avoid conflicts with the new
162 * built-in profile "Sonar way", which has probably different configuration.
164 private void renameOutdatedProfiles(DbSession dbSession, BuiltInQProfile profile) {
165 Collection<String> uuids = dbClient.qualityProfileDao().selectUuidsOfCustomRulesProfiles(dbSession, profile.getLanguage(), profile.getName());
166 if (uuids.isEmpty()) {
169 Profiler profiler = Profiler.createIfDebug(Loggers.get(getClass())).start();
170 String newName = profile.getName() + " (outdated copy)";
171 LOGGER.info("Rename Quality profiles [{}/{}] to [{}]", profile.getLanguage(), profile.getName(), newName);
172 dbClient.qualityProfileDao().renameRulesProfilesAndCommit(dbSession, uuids, newName);
173 profiler.stopDebug(format("%d Quality profiles renamed to [%s]", uuids.size(), newName));
177 * This method ensure that after plugin removal, we don't end-up in a situation where a custom default quality profile is set,
178 * and cannot be deleted because builtIn quality profile cannot be set to default.
180 * @see <a href="https://jira.sonarsource.com/browse/SONAR-19478">SONAR-19478</a>
182 private void ensureBuiltInAreDefaultQPWhenNoRules(DbSession dbSession) {
183 Set<String> activeLanguages = Arrays.stream(languages.all()).map(Language::getKey).collect(toSet());
184 Map<String, RulesProfileDto> builtInQProfileByLanguage = dbClient.qualityProfileDao().selectBuiltInRuleProfiles(dbSession).stream()
185 .collect(toMap(RulesProfileDto::getLanguage, Function.identity(), (oldValue, newValue) -> oldValue));
186 List<QProfileDto> defaultProfileWithNoRules = dbClient.qualityProfileDao().selectDefaultProfilesWithoutActiveRules(dbSession, activeLanguages, false);
188 for (QProfileDto defaultProfileWithNoRule : defaultProfileWithNoRules) {
189 long rulesCountByLanguage = dbClient.ruleDao().countByLanguage(dbSession, defaultProfileWithNoRule.getLanguage());
190 RulesProfileDto builtInQProfile = builtInQProfileByLanguage.get(defaultProfileWithNoRule.getLanguage());
191 if (builtInQProfile != null && rulesCountByLanguage == 0) {
192 QProfileDto builtInQualityProfile = dbClient.qualityProfileDao().selectByRuleProfileUuid(dbSession, builtInQProfile.getUuid());
193 if (builtInQualityProfile != null) {
194 reassignDefaultQualityProfile(dbSession, defaultProfileWithNoRule, builtInQualityProfile);
201 * This method ensure that if a default built-in quality profile does not have any active rules but another built-in one for the same language
202 * does have active rules, the last one will be the default one.
204 * @see <a href="https://jira.sonarsource.com/browse/SONAR-10363">SONAR-10363</a>
206 private void ensureBuiltInDefaultQPContainsRules(DbSession dbSession) {
207 Map<String, RulesProfileDto> rulesProfilesByLanguage = dbClient.qualityProfileDao().selectBuiltInRuleProfilesWithActiveRules(dbSession).stream()
208 .collect(toMap(RulesProfileDto::getLanguage, Function.identity(), (oldValue, newValue) -> oldValue));
210 dbClient.qualityProfileDao().selectDefaultProfilesWithoutActiveRules(dbSession, rulesProfilesByLanguage.keySet(), true)
212 RulesProfileDto rulesProfile = rulesProfilesByLanguage.get(qp.getLanguage());
213 if (rulesProfile == null) {
217 QProfileDto qualityProfile = dbClient.qualityProfileDao().selectByRuleProfileUuid(dbSession, rulesProfile.getUuid());
218 if (qualityProfile == null) {
222 reassignDefaultQualityProfile(dbSession, qp, qualityProfile);
224 LOGGER.info("Default built-in quality profile for language [{}] has been updated from [{}] to [{}] since previous default does not have active rules.",
227 rulesProfile.getName());
231 private void reassignDefaultQualityProfile(DbSession dbSession, QProfileDto currentDefaultQualityProfile, QProfileDto newDefaultQualityProfile) {
232 Set<String> uuids = dbClient.defaultQProfileDao().selectExistingQProfileUuids(dbSession, Collections.singleton(currentDefaultQualityProfile.getKee()));
233 dbClient.defaultQProfileDao().deleteByQProfileUuids(dbSession, uuids);
234 dbClient.defaultQProfileDao().insertOrUpdate(dbSession, new DefaultQProfileDto()
235 .setQProfileUuid(newDefaultQualityProfile.getKee())
236 .setLanguage(currentDefaultQualityProfile.getLanguage()));
239 public void unsetBuiltInFlagAndRenameQPWhenPluginUninstalled(DbSession dbSession) {
240 Set<QProfileName> pluginsBuiltInQProfiles = builtInQProfileRepository.get()
242 .map(BuiltInQProfile::getQProfileName)
245 Set<String> qualityProfileLanguages = pluginsBuiltInQProfiles
247 .map(QProfileName::getLanguage)
250 dbClient.qualityProfileDao().selectBuiltInRuleProfiles(dbSession)
251 .forEach(qProfileDto -> {
252 var dbProfileName = QProfileName.createFor(qProfileDto.getLanguage(), qProfileDto.getName());
254 // Built-in Quality Profile can be a leftover from plugin which has been removed
255 // Rename Quality Profile and unset built-in flag allowing Quality Profile for existing languages to be removed
256 // Quality Profiles for languages not existing anymore are marked as 'REMOVED' and won't be seen in UI
257 if (!pluginsBuiltInQProfiles.contains(dbProfileName) && qualityProfileLanguages.contains(qProfileDto.getLanguage())) {
258 String oldName = qProfileDto.getName();
259 String newName = generateNewProfileName(qProfileDto);
260 qProfileDto.setName(newName);
261 qProfileDto.setIsBuiltIn(false);
262 dbClient.qualityProfileDao().update(dbSession, qProfileDto);
264 LOGGER.info("Quality profile [{}] for language [{}] is no longer built-in and has been renamed to [{}] "
265 + "since it does not have any active rules.",
267 dbProfileName.getLanguage(),
274 * Abbreviate Quality Profile name if it will be too long with prefix and append suffix
276 private String generateNewProfileName(RulesProfileDto qProfileDto) {
277 var shortName = StringUtils.abbreviate(qProfileDto.getName(), 40);
278 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM dd yyyy 'at' hh:mm a")
279 .withLocale(Locale.getDefault())
280 .withZone(ZoneId.systemDefault());
281 var now = formatter.format(Instant.ofEpochMilli(system2.now()));
282 String suffix = " (outdated copy since " + now + ")";
283 return shortName + suffix;