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.rule.registration;
22 import com.google.common.collect.Sets;
23 import java.util.HashMap;
24 import java.util.List;
26 import java.util.Objects;
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;
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;
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.
65 public class StartupRuleUpdater {
67 private static final Logger LOG = Loggers.get(StartupRuleUpdater.class);
69 private final DbClient dbClient;
70 private final System2 system2;
71 private final UuidFactory uuidFactory;
72 private final RuleDescriptionSectionsGeneratorResolver sectionsGeneratorResolver;
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;
83 * Returns true in case there was any change detected between rule in the database and rule from the plugin.
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;
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);
100 // DeprecatedKeys that must be deleted
101 List<String> uuidsToBeDeleted = difference(deprecatedRuleKeysFromDB, deprecatedRuleKeysFromDefinition).stream()
102 .map(SingleDeprecatedRuleKey::getUuid)
105 dbClient.ruleDao().deleteDeprecatedRuleKeys(dbSession, uuidsToBeDeleted);
107 // DeprecatedKeys that must be created
108 Sets.SetView<SingleDeprecatedRuleKey> deprecatedRuleKeysToBeCreated = difference(deprecatedRuleKeysFromDefinition, deprecatedRuleKeysFromDB);
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())));
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());
125 if (mergeDescription(def, dto)) {
128 if (!Objects.equals(dto.getPluginKey(), def.pluginKey())) {
129 dto.setPluginKey(def.pluginKey());
132 if (!Objects.equals(dto.getConfigKey(), def.internalKey())) {
133 dto.setConfigKey(def.internalKey());
136 String severity = def.severity();
137 if (!Objects.equals(dto.getSeverityString(), severity)) {
138 dto.setSeverity(severity);
141 boolean isTemplate = def.template();
142 if (isTemplate != dto.isTemplate()) {
143 dto.setIsTemplate(isTemplate);
146 if (def.status() != dto.getStatus()) {
147 dto.setStatus(def.status());
150 if (!Objects.equals(dto.getScope().name(), def.scope().name())) {
151 dto.setScope(RuleDto.Scope.valueOf(def.scope().name()));
154 if (!Objects.equals(dto.getLanguage(), def.repository().language())) {
155 dto.setLanguage(def.repository().language());
158 RuleType type = RuleType.valueOf(def.type().name());
159 if (!Objects.equals(dto.getType(), type.getDbConstant())) {
163 changed |= mergeCleanCodeAttribute(def, dto, ruleChange);
164 changed |= mergeImpacts(def, dto, ruleChange);
166 dto.setIsAdHoc(false);
172 private static boolean mergeCleanCodeAttribute(RulesDefinition.Rule def, RuleDto dto, RuleChange ruleChange) {
173 if (dto.getEnumType() == RuleType.SECURITY_HOTSPOT) {
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);
183 // apply non-nullable default
184 if (dto.getCleanCodeAttribute() == null) {
185 dto.setCleanCodeAttribute(CleanCodeAttribute.defaultCleanCodeAttribute());
191 boolean mergeImpacts(RulesDefinition.Rule def, RuleDto dto, RuleChange ruleChange) {
192 if (dto.getEnumType() == RuleType.SECURITY_HOTSPOT) {
196 Map<SoftwareQuality, Severity> impactsFromPlugin = def.defaultImpacts();
197 Map<SoftwareQuality, Severity> impactsFromDb = dto.getDefaultImpacts().stream().collect(Collectors.toMap(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity));
199 if (impactsFromPlugin.isEmpty()) {
200 throw new IllegalStateException("There should be at least one impact defined for the rule " + def.key());
203 if (!Objects.equals(impactsFromDb, impactsFromPlugin)) {
204 dto.replaceAllDefaultImpacts(impactsFromPlugin.entrySet()
206 .map(e -> new ImpactDto().setSoftwareQuality(e.getKey()).setSeverity(e.getValue()))
207 .collect(Collectors.toSet()));
208 ruleChange.addImpactsChange(removeDuplicatedImpacts(impactsFromDb, impactsFromPlugin), removeDuplicatedImpacts(impactsFromPlugin, impactsFromDb));
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.
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));
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());
235 private static boolean mergeTags(RulesDefinition.Rule ruleDef, RuleDto dto) {
236 boolean changed = false;
238 if (RuleStatus.REMOVED == ruleDef.status()) {
239 dto.setSystemTags(emptySet());
241 } else if (dto.getSystemTags().size() != ruleDef.tags().size() || !dto.getSystemTags().containsAll(ruleDef.tags())) {
242 dto.setSystemTags(ruleDef.tags());
248 private static boolean mergeSecurityStandards(RulesDefinition.Rule ruleDef, RuleDto dto) {
249 boolean changed = false;
250 Set<String> securityStandards = dto.getSecurityStandards();
252 if (RuleStatus.REMOVED == ruleDef.status()) {
253 dto.setSecurityStandards(emptySet());
255 } else if (securityStandards.size() != ruleDef.securityStandards().size() || !securityStandards.containsAll(ruleDef.securityStandards())) {
256 dto.setSecurityStandards(ruleDef.securityStandards());
262 private static boolean containsHtmlDescription(RulesDefinition.Rule rule) {
263 return isNotEmpty(rule.htmlDescription()) || !rule.ruleDescriptionSections().isEmpty();
266 private static boolean ruleDescriptionSectionsUnchanged(RuleDto ruleDto, Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos) {
267 if (ruleDto.getRuleDescriptionSectionDtos().size() != newRuleDescriptionSectionDtos.size()) {
270 return ruleDto.getRuleDescriptionSectionDtos().stream()
271 .allMatch(sectionDto -> contains(newRuleDescriptionSectionDtos, sectionDto));
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()));
280 private static boolean mergeDebtDefinitions(RuleDto dto, @Nullable String remediationFunction,
281 @Nullable String remediationCoefficient, @Nullable String remediationOffset, @Nullable String gapDescription) {
282 boolean changed = false;
284 if (!Objects.equals(dto.getDefRemediationFunction(), remediationFunction)) {
285 dto.setDefRemediationFunction(remediationFunction);
288 if (!Objects.equals(dto.getDefRemediationGapMultiplier(), remediationCoefficient)) {
289 dto.setDefRemediationGapMultiplier(remediationCoefficient);
292 if (!Objects.equals(dto.getDefRemediationBaseEffort(), remediationOffset)) {
293 dto.setDefRemediationBaseEffort(remediationOffset);
296 if (!Objects.equals(dto.getGapDescription(), gapDescription)) {
297 dto.setGapDescription(gapDescription);
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;
308 return mergeDebtDefinitions(dto,
309 debtRemediationFunction.type().name(),
310 debtRemediationFunction.gapMultiplier(),
311 debtRemediationFunction.baseEffort(),
312 def.gapDescription());
314 return mergeDebtDefinitions(dto, null, null, null, null);
317 private boolean mergeDescription(RulesDefinition.Rule rule, RuleDto ruleDto) {
318 Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos = sectionsGeneratorResolver.generateFor(rule);
319 if (ruleDescriptionSectionsUnchanged(ruleDto, newRuleDescriptionSectionDtos)) {
322 ruleDto.replaceRuleDescriptionSectionDtos(newRuleDescriptionSectionDtos);
323 if (containsHtmlDescription(rule)) {
324 ruleDto.setDescriptionFormat(RuleDto.Format.HTML);
326 } else if (isNotEmpty(rule.markdownDescription())) {
327 ruleDto.setDescriptionFormat(RuleDto.Format.MARKDOWN);
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<>();
337 Profiler profiler = Profiler.create(LOG);
338 for (RuleParamDto paramDto : paramDtos) {
339 RulesDefinition.Param paramDef = ruleDef.param(paramDto.getName());
340 if (paramDef == null) {
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());
346 if (mergeParam(paramDto, paramDef)) {
347 dbClient.ruleDao().updateRuleParam(session, rule, paramDto);
349 existingParamsByName.put(paramDto.getName(), paramDto);
353 // Create newly parameters
354 for (RulesDefinition.Param param : ruleDef.params()) {
355 RuleParamDto paramDto = existingParamsByName.get(param.key());
356 if (paramDto != null) {
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())) {
368 // Propagate the default value to existing active rule parameters
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);
374 profiler.stopDebug(format("Propagate new param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
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());
384 if (!Objects.equals(paramDto.getDefaultValue(), paramDef.defaultValue())) {
385 paramDto.setDefaultValue(paramDef.defaultValue());
388 if (!Objects.equals(paramDto.getDescription(), paramDef.description())) {
389 paramDto.setDescription(paramDef.description());
395 public static class RuleChange {
396 private boolean ruleDefinitionChanged = false;
397 private final String ruleUuid;
398 private PluginRuleUpdate pluginRuleUpdate;
400 public RuleChange(RuleDto ruleDto) {
401 this.ruleUuid = ruleDto.getUuid();
404 private void createPluginRuleUpdateIfNeeded() {
405 if (pluginRuleUpdate == null) {
406 pluginRuleUpdate = new PluginRuleUpdate();
407 pluginRuleUpdate.setRuleUuid(ruleUuid);
411 public void addImpactsChange(Map<SoftwareQuality, Severity> oldImpacts, Map<SoftwareQuality, Severity> newImpacts) {
412 createPluginRuleUpdateIfNeeded();
413 oldImpacts.forEach(pluginRuleUpdate::addOldImpact);
414 newImpacts.forEach(pluginRuleUpdate::addNewImpact);
417 public void addCleanCodeAttributeChange(@Nullable CleanCodeAttribute oldAttribute, @Nullable CleanCodeAttribute newAttribute) {
418 createPluginRuleUpdateIfNeeded();
419 pluginRuleUpdate.setOldCleanCodeAttribute(oldAttribute);
420 pluginRuleUpdate.setNewCleanCodeAttribute(newAttribute);
423 public boolean hasRuleDefinitionChanged() {
424 return ruleDefinitionChanged;
428 public PluginRuleUpdate getPluginRuleUpdate() {
429 return pluginRuleUpdate;