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