]> source.dussan.org Git - sonarqube.git/blob
d6332e11e905dd5d6bf52b66ab8fa78d7ba59a98
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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.rule.registration;
21
22 import com.google.common.collect.Sets;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.Set;
28 import java.util.stream.Collectors;
29 import javax.annotation.Nullable;
30 import org.apache.commons.lang.StringUtils;
31 import org.sonar.api.issue.impact.Severity;
32 import org.sonar.api.issue.impact.SoftwareQuality;
33 import org.sonar.api.rule.RuleStatus;
34 import org.sonar.api.rules.CleanCodeAttribute;
35 import org.sonar.api.rules.RuleType;
36 import org.sonar.api.server.debt.DebtRemediationFunction;
37 import org.sonar.api.server.rule.RulesDefinition;
38 import org.sonar.api.utils.System2;
39 import org.sonar.api.utils.log.Logger;
40 import org.sonar.api.utils.log.Loggers;
41 import org.sonar.api.utils.log.Profiler;
42 import org.sonar.core.util.UuidFactory;
43 import org.sonar.db.DbClient;
44 import org.sonar.db.DbSession;
45 import org.sonar.db.issue.ImpactDto;
46 import org.sonar.db.qualityprofile.ActiveRuleDto;
47 import org.sonar.db.qualityprofile.ActiveRuleParamDto;
48 import org.sonar.db.rule.DeprecatedRuleKeyDto;
49 import org.sonar.db.rule.RuleDescriptionSectionDto;
50 import org.sonar.db.rule.RuleDto;
51 import org.sonar.db.rule.RuleParamDto;
52 import org.sonar.server.rule.RuleDescriptionSectionsGeneratorResolver;
53
54 import static com.google.common.collect.Sets.difference;
55 import static java.lang.String.format;
56 import static java.util.Collections.emptySet;
57 import static org.apache.commons.lang.StringUtils.isNotEmpty;
58
59 /**
60  * The class detects changes between the rule definition coming from plugins during startup and rule from database.
61  * In case any changes are detected the rule is updated with the new information from plugin.
62  */
63 public class StartupRuleUpdater {
64
65   private static final Logger LOG = Loggers.get(StartupRuleUpdater.class);
66
67   private final DbClient dbClient;
68   private final System2 system2;
69   private final UuidFactory uuidFactory;
70   private final RuleDescriptionSectionsGeneratorResolver sectionsGeneratorResolver;
71
72   public StartupRuleUpdater(DbClient dbClient, System2 system2, UuidFactory uuidFactory,
73     RuleDescriptionSectionsGeneratorResolver sectionsGeneratorResolver) {
74     this.dbClient = dbClient;
75     this.system2 = system2;
76     this.uuidFactory = uuidFactory;
77     this.sectionsGeneratorResolver = sectionsGeneratorResolver;
78   }
79
80   /**
81    * Returns true in case there was any change detected between rule in the database and rule from the plugin.
82    */
83   boolean findChangesAndUpdateRule(RulesDefinition.Rule ruleDef, RuleDto ruleDto) {
84     boolean ruleMerged = mergeRule(ruleDef, ruleDto);
85     boolean debtDefinitionsMerged = mergeDebtDefinitions(ruleDef, ruleDto);
86     boolean tagsMerged = mergeTags(ruleDef, ruleDto);
87     boolean securityStandardsMerged = mergeSecurityStandards(ruleDef, ruleDto);
88     boolean educationPrinciplesMerged = mergeEducationPrinciples(ruleDef, ruleDto);
89     return ruleMerged || debtDefinitionsMerged || tagsMerged || securityStandardsMerged || educationPrinciplesMerged;
90   }
91
92   void updateDeprecatedKeys(RulesRegistrationContext context, RulesDefinition.Rule ruleDef, RuleDto rule, DbSession dbSession) {
93     Set<SingleDeprecatedRuleKey> deprecatedRuleKeysFromDefinition = SingleDeprecatedRuleKey.from(ruleDef);
94     Set<SingleDeprecatedRuleKey> deprecatedRuleKeysFromDB = context.getDBDeprecatedKeysFor(rule);
95
96     // DeprecatedKeys that must be deleted
97     List<String> uuidsToBeDeleted = difference(deprecatedRuleKeysFromDB, deprecatedRuleKeysFromDefinition).stream()
98       .map(SingleDeprecatedRuleKey::getUuid)
99       .toList();
100
101     dbClient.ruleDao().deleteDeprecatedRuleKeys(dbSession, uuidsToBeDeleted);
102
103     // DeprecatedKeys that must be created
104     Sets.SetView<SingleDeprecatedRuleKey> deprecatedRuleKeysToBeCreated = difference(deprecatedRuleKeysFromDefinition, deprecatedRuleKeysFromDB);
105
106     deprecatedRuleKeysToBeCreated
107       .forEach(r -> dbClient.ruleDao().insert(dbSession, new DeprecatedRuleKeyDto()
108         .setUuid(uuidFactory.create())
109         .setRuleUuid(rule.getUuid())
110         .setOldRepositoryKey(r.getOldRepositoryKey())
111         .setOldRuleKey(r.getOldRuleKey())
112         .setCreatedAt(system2.now())));
113   }
114
115   private boolean mergeRule(RulesDefinition.Rule def, RuleDto dto) {
116     boolean changed = false;
117     if (!Objects.equals(dto.getName(), def.name())) {
118       dto.setName(def.name());
119       changed = true;
120     }
121     if (mergeDescription(def, dto)) {
122       changed = true;
123     }
124     if (!Objects.equals(dto.getPluginKey(), def.pluginKey())) {
125       dto.setPluginKey(def.pluginKey());
126       changed = true;
127     }
128     if (!Objects.equals(dto.getConfigKey(), def.internalKey())) {
129       dto.setConfigKey(def.internalKey());
130       changed = true;
131     }
132     String severity = def.severity();
133     if (!Objects.equals(dto.getSeverityString(), severity)) {
134       dto.setSeverity(severity);
135       changed = true;
136     }
137     boolean isTemplate = def.template();
138     if (isTemplate != dto.isTemplate()) {
139       dto.setIsTemplate(isTemplate);
140       changed = true;
141     }
142     if (def.status() != dto.getStatus()) {
143       dto.setStatus(def.status());
144       changed = true;
145     }
146     if (!Objects.equals(dto.getScope().name(), def.scope().name())) {
147       dto.setScope(RuleDto.Scope.valueOf(def.scope().name()));
148       changed = true;
149     }
150     if (!Objects.equals(dto.getLanguage(), def.repository().language())) {
151       dto.setLanguage(def.repository().language());
152       changed = true;
153     }
154     RuleType type = RuleType.valueOf(def.type().name());
155     if (!Objects.equals(dto.getType(), type.getDbConstant())) {
156       dto.setType(type);
157       changed = true;
158     }
159     changed |= mergeCleanCodeAttribute(def, dto);
160     changed |= mergeImpacts(def, dto, uuidFactory);
161     if (dto.isAdHoc()) {
162       dto.setIsAdHoc(false);
163       changed = true;
164     }
165     return changed;
166   }
167
168   private static boolean mergeCleanCodeAttribute(RulesDefinition.Rule def, RuleDto dto) {
169     boolean changed = false;
170     CleanCodeAttribute defCleanCodeAttribute = def.cleanCodeAttribute();
171     if (!Objects.equals(dto.getCleanCodeAttribute(), defCleanCodeAttribute) && (defCleanCodeAttribute != null)) {
172       dto.setCleanCodeAttribute(defCleanCodeAttribute);
173       changed = true;
174     }
175     // apply non-nullable default
176     if (dto.getCleanCodeAttribute() == null) {
177       dto.setCleanCodeAttribute(CleanCodeAttribute.defaultCleanCodeAttribute());
178       changed = true;
179     }
180     return changed;
181   }
182
183   boolean mergeImpacts(RulesDefinition.Rule def, RuleDto dto, UuidFactory uuidFactory) {
184     if (dto.getEnumType() == RuleType.SECURITY_HOTSPOT) {
185       return false;
186     }
187
188     Map<SoftwareQuality, Severity> impactsFromPlugin = def.defaultImpacts();
189     Map<SoftwareQuality, Severity> impactsFromDb = dto.getDefaultImpacts().stream().collect(Collectors.toMap(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity));
190
191     if (impactsFromPlugin.isEmpty()) {
192       throw new IllegalStateException("There should be at least one impact defined for the rule " + def.key());
193     }
194
195     if (!Objects.equals(impactsFromDb, impactsFromPlugin)) {
196       dto.replaceAllDefaultImpacts(impactsFromPlugin.entrySet()
197         .stream()
198         .map(e -> new ImpactDto().setUuid(uuidFactory.create()).setSoftwareQuality(e.getKey()).setSeverity(e.getValue()))
199         .collect(Collectors.toSet()));
200       return true;
201     }
202
203     return false;
204   }
205
206   private static boolean mergeEducationPrinciples(RulesDefinition.Rule ruleDef, RuleDto dto) {
207     boolean changed = false;
208     if (dto.getEducationPrinciples().size() != ruleDef.educationPrincipleKeys().size() ||
209         !dto.getEducationPrinciples().containsAll(ruleDef.educationPrincipleKeys())) {
210       dto.setEducationPrinciples(ruleDef.educationPrincipleKeys());
211       changed = true;
212     }
213     return changed;
214   }
215
216   private static boolean mergeTags(RulesDefinition.Rule ruleDef, RuleDto dto) {
217     boolean changed = false;
218
219     if (RuleStatus.REMOVED == ruleDef.status()) {
220       dto.setSystemTags(emptySet());
221       changed = true;
222     } else if (dto.getSystemTags().size() != ruleDef.tags().size() ||
223                !dto.getSystemTags().containsAll(ruleDef.tags())) {
224       dto.setSystemTags(ruleDef.tags());
225       changed = true;
226     }
227     return changed;
228   }
229
230   private static boolean mergeSecurityStandards(RulesDefinition.Rule ruleDef, RuleDto dto) {
231     boolean changed = false;
232
233     if (RuleStatus.REMOVED == ruleDef.status()) {
234       dto.setSecurityStandards(emptySet());
235       changed = true;
236     } else if (dto.getSecurityStandards().size() != ruleDef.securityStandards().size() ||
237                !dto.getSecurityStandards().containsAll(ruleDef.securityStandards())) {
238       dto.setSecurityStandards(ruleDef.securityStandards());
239       changed = true;
240     }
241     return changed;
242   }
243
244   private static boolean containsHtmlDescription(RulesDefinition.Rule rule) {
245     return isNotEmpty(rule.htmlDescription()) || !rule.ruleDescriptionSections().isEmpty();
246   }
247
248   private static boolean ruleDescriptionSectionsUnchanged(RuleDto ruleDto, Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos) {
249     if (ruleDto.getRuleDescriptionSectionDtos().size() != newRuleDescriptionSectionDtos.size()) {
250       return false;
251     }
252     return ruleDto.getRuleDescriptionSectionDtos().stream()
253       .allMatch(sectionDto -> contains(newRuleDescriptionSectionDtos, sectionDto));
254   }
255
256   private static boolean contains(Set<RuleDescriptionSectionDto> sectionDtos, RuleDescriptionSectionDto sectionDto) {
257     return sectionDtos.stream()
258       .filter(s -> s.getKey().equals(sectionDto.getKey()) && s.getContent().equals(sectionDto.getContent()))
259       .anyMatch(s -> Objects.equals(s.getContext(), sectionDto.getContext()));
260   }
261
262   private static boolean mergeDebtDefinitions(RuleDto dto, @Nullable String remediationFunction,
263     @Nullable String remediationCoefficient, @Nullable String remediationOffset, @Nullable String gapDescription) {
264     boolean changed = false;
265
266     if (!Objects.equals(dto.getDefRemediationFunction(), remediationFunction)) {
267       dto.setDefRemediationFunction(remediationFunction);
268       changed = true;
269     }
270     if (!Objects.equals(dto.getDefRemediationGapMultiplier(), remediationCoefficient)) {
271       dto.setDefRemediationGapMultiplier(remediationCoefficient);
272       changed = true;
273     }
274     if (!Objects.equals(dto.getDefRemediationBaseEffort(), remediationOffset)) {
275       dto.setDefRemediationBaseEffort(remediationOffset);
276       changed = true;
277     }
278     if (!Objects.equals(dto.getGapDescription(), gapDescription)) {
279       dto.setGapDescription(gapDescription);
280       changed = true;
281     }
282     return changed;
283   }
284
285   private static boolean mergeDebtDefinitions(RulesDefinition.Rule def, RuleDto dto) {
286     // Debt definitions are set to null if the sub-characteristic and the remediation function are null
287     DebtRemediationFunction debtRemediationFunction = def.debtRemediationFunction();
288     boolean hasDebt = debtRemediationFunction != null;
289     if (hasDebt) {
290       return mergeDebtDefinitions(dto,
291         debtRemediationFunction.type().name(),
292         debtRemediationFunction.gapMultiplier(),
293         debtRemediationFunction.baseEffort(),
294         def.gapDescription());
295     }
296     return mergeDebtDefinitions(dto, null, null, null, null);
297   }
298
299   private boolean mergeDescription(RulesDefinition.Rule rule, RuleDto ruleDto) {
300     Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos = sectionsGeneratorResolver.generateFor(rule);
301     if (ruleDescriptionSectionsUnchanged(ruleDto, newRuleDescriptionSectionDtos)) {
302       return false;
303     }
304     ruleDto.replaceRuleDescriptionSectionDtos(newRuleDescriptionSectionDtos);
305     if (containsHtmlDescription(rule)) {
306       ruleDto.setDescriptionFormat(RuleDto.Format.HTML);
307       return true;
308     } else if (isNotEmpty(rule.markdownDescription())) {
309       ruleDto.setDescriptionFormat(RuleDto.Format.MARKDOWN);
310       return true;
311     }
312     return false;
313   }
314
315   void mergeParams(RulesRegistrationContext context, RulesDefinition.Rule ruleDef, RuleDto rule, DbSession session) {
316     List<RuleParamDto> paramDtos = context.getRuleParametersFor(rule.getUuid());
317     Map<String, RuleParamDto> existingParamsByName = new HashMap<>();
318
319     Profiler profiler = Profiler.create(LOG);
320     for (RuleParamDto paramDto : paramDtos) {
321       RulesDefinition.Param paramDef = ruleDef.param(paramDto.getName());
322       if (paramDef == null) {
323         profiler.start();
324         dbClient.activeRuleDao().deleteParamsByRuleParam(session, paramDto);
325         profiler.stopDebug(format("Propagate deleted param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
326         dbClient.ruleDao().deleteRuleParam(session, paramDto.getUuid());
327       } else {
328         if (mergeParam(paramDto, paramDef)) {
329           dbClient.ruleDao().updateRuleParam(session, rule, paramDto);
330         }
331         existingParamsByName.put(paramDto.getName(), paramDto);
332       }
333     }
334
335     // Create newly parameters
336     for (RulesDefinition.Param param : ruleDef.params()) {
337       RuleParamDto paramDto = existingParamsByName.get(param.key());
338       if (paramDto != null) {
339         continue;
340       }
341       paramDto = RuleParamDto.createFor(rule)
342         .setName(param.key())
343         .setDescription(param.description())
344         .setDefaultValue(param.defaultValue())
345         .setType(param.type().toString());
346       dbClient.ruleDao().insertRuleParam(session, rule, paramDto);
347       if (StringUtils.isEmpty(param.defaultValue())) {
348         continue;
349       }
350       // Propagate the default value to existing active rule parameters
351       profiler.start();
352       for (ActiveRuleDto activeRule : dbClient.activeRuleDao().selectByRuleUuid(session, rule.getUuid())) {
353         ActiveRuleParamDto activeParam = ActiveRuleParamDto.createFor(paramDto).setValue(param.defaultValue());
354         dbClient.activeRuleDao().insertParam(session, activeRule, activeParam);
355       }
356       profiler.stopDebug(format("Propagate new param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
357     }
358   }
359
360   private static boolean mergeParam(RuleParamDto paramDto, RulesDefinition.Param paramDef) {
361     boolean changed = false;
362     if (!Objects.equals(paramDto.getType(), paramDef.type().toString())) {
363       paramDto.setType(paramDef.type().toString());
364       changed = true;
365     }
366     if (!Objects.equals(paramDto.getDefaultValue(), paramDef.defaultValue())) {
367       paramDto.setDefaultValue(paramDef.defaultValue());
368       changed = true;
369     }
370     if (!Objects.equals(paramDto.getDescription(), paramDef.description())) {
371       paramDto.setDescription(paramDef.description());
372       changed = true;
373     }
374     return changed;
375   }
376
377 }