]> source.dussan.org Git - sonarqube.git/blob
e84b3ba71afbde3b5240c4e68209144d63f49fa6
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.server.qualityprofile;
21
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;
32 import java.util.Map;
33 import java.util.Set;
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;
55
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;
60
61 /**
62  * Synchronize Quality profiles during server startup
63  */
64 @ServerSide
65 public class RegisterQualityProfiles implements Startable {
66
67   private static final Logger LOGGER = Loggers.get(RegisterQualityProfiles.class);
68
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;
76
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;
87   }
88
89   @Override
90   public void start() {
91     List<BuiltInQProfile> builtInQProfiles = builtInQProfileRepository.get();
92     if (builtInQProfiles.isEmpty()) {
93       return;
94     }
95
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();
100
101       Map<QProfileName, RulesProfileDto> persistedRuleProfiles = loadPersistedProfiles(dbSession);
102
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);
108         } else {
109           List<ActiveRuleChange> changes = update(dbSession, builtIn, ruleProfile);
110           changedProfiles.putAll(builtIn.getQProfileName(), changes.stream()
111             .filter(change -> {
112               String inheritance = change.getActiveRule().getInheritance();
113               return inheritance == null || NONE.name().equals(inheritance);
114             })
115             .toList());
116         }
117       });
118       if (!changedProfiles.isEmpty()) {
119         long endDate = system2.now();
120         builtInQualityProfilesNotification.onChange(changedProfiles, startDate, endDate);
121       }
122       ensureBuiltInDefaultQPContainsRules(dbSession);
123       unsetBuiltInFlagAndRenameQPWhenPluginUninstalled(dbSession);
124       ensureBuiltInAreDefaultQPWhenNoRules(dbSession);
125
126       dbSession.commit();
127     }
128     profiler.stopDebug();
129   }
130
131   @Override
132   public void stop() {
133     // nothing to do
134   }
135
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()));
139   }
140
141   private void create(DbSession dbSession, DbSession batchDbSession, BuiltInQProfile builtIn) {
142     LOGGER.info("Register profile {}", builtIn.getQProfileName());
143
144     renameOutdatedProfiles(dbSession, builtIn);
145
146     builtInQProfileInsert.create(batchDbSession, builtIn);
147   }
148
149   private List<ActiveRuleChange> update(DbSession dbSession, BuiltInQProfile definition, RulesProfileDto dbProfile) {
150     LOGGER.info("Update profile {}", definition.getQProfileName());
151
152     return builtInQProfileUpdate.update(dbSession, definition, dbProfile);
153   }
154
155   /**
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.
158    * <p>
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.
163    */
164   private void renameOutdatedProfiles(DbSession dbSession, BuiltInQProfile profile) {
165     Collection<String> uuids = dbClient.qualityProfileDao().selectUuidsOfCustomRulesProfiles(dbSession, profile.getLanguage(), profile.getName());
166     if (uuids.isEmpty()) {
167       return;
168     }
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));
174   }
175
176   /**
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.
179    *
180    * @see <a href="https://jira.sonarsource.com/browse/SONAR-19478">SONAR-19478</a>
181    */
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);
187
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);
195         }
196       }
197     }
198   }
199
200   /**
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.
203    *
204    * @see <a href="https://jira.sonarsource.com/browse/SONAR-10363">SONAR-10363</a>
205    */
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));
209
210     dbClient.qualityProfileDao().selectDefaultProfilesWithoutActiveRules(dbSession, rulesProfilesByLanguage.keySet(), true)
211       .forEach(qp -> {
212         RulesProfileDto rulesProfile = rulesProfilesByLanguage.get(qp.getLanguage());
213         if (rulesProfile == null) {
214           return;
215         }
216
217         QProfileDto qualityProfile = dbClient.qualityProfileDao().selectByRuleProfileUuid(dbSession, rulesProfile.getUuid());
218         if (qualityProfile == null) {
219           return;
220         }
221
222         reassignDefaultQualityProfile(dbSession, qp, qualityProfile);
223
224         LOGGER.info("Default built-in quality profile for language [{}] has been updated from [{}] to [{}] since previous default does not have active rules.",
225           qp.getLanguage(),
226           qp.getName(),
227           rulesProfile.getName());
228       });
229   }
230
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()));
237   }
238
239   public void unsetBuiltInFlagAndRenameQPWhenPluginUninstalled(DbSession dbSession) {
240     Set<QProfileName> pluginsBuiltInQProfiles = builtInQProfileRepository.get()
241       .stream()
242       .map(BuiltInQProfile::getQProfileName)
243       .collect(toSet());
244
245     Set<String> qualityProfileLanguages = pluginsBuiltInQProfiles
246       .stream()
247       .map(QProfileName::getLanguage)
248       .collect(toSet());
249
250     dbClient.qualityProfileDao().selectBuiltInRuleProfiles(dbSession)
251       .forEach(qProfileDto -> {
252         var dbProfileName = QProfileName.createFor(qProfileDto.getLanguage(), qProfileDto.getName());
253
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);
263
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.",
266             oldName,
267             dbProfileName.getLanguage(),
268             newName);
269         }
270       });
271   }
272
273   /**
274    * Abbreviate Quality Profile name if it will be too long with prefix and append suffix
275    */
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;
284   }
285 }