3 * Copyright (C) 2009-2023 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.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;
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;
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.
63 public class StartupRuleUpdater {
65 private static final Logger LOG = Loggers.get(StartupRuleUpdater.class);
67 private final DbClient dbClient;
68 private final System2 system2;
69 private final UuidFactory uuidFactory;
70 private final RuleDescriptionSectionsGeneratorResolver sectionsGeneratorResolver;
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;
81 * Returns true in case there was any change detected between rule in the database and rule from the plugin.
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;
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);
96 // DeprecatedKeys that must be deleted
97 List<String> uuidsToBeDeleted = difference(deprecatedRuleKeysFromDB, deprecatedRuleKeysFromDefinition).stream()
98 .map(SingleDeprecatedRuleKey::getUuid)
101 dbClient.ruleDao().deleteDeprecatedRuleKeys(dbSession, uuidsToBeDeleted);
103 // DeprecatedKeys that must be created
104 Sets.SetView<SingleDeprecatedRuleKey> deprecatedRuleKeysToBeCreated = difference(deprecatedRuleKeysFromDefinition, deprecatedRuleKeysFromDB);
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())));
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());
121 if (mergeDescription(def, dto)) {
124 if (!Objects.equals(dto.getPluginKey(), def.pluginKey())) {
125 dto.setPluginKey(def.pluginKey());
128 if (!Objects.equals(dto.getConfigKey(), def.internalKey())) {
129 dto.setConfigKey(def.internalKey());
132 String severity = def.severity();
133 if (!Objects.equals(dto.getSeverityString(), severity)) {
134 dto.setSeverity(severity);
137 boolean isTemplate = def.template();
138 if (isTemplate != dto.isTemplate()) {
139 dto.setIsTemplate(isTemplate);
142 if (def.status() != dto.getStatus()) {
143 dto.setStatus(def.status());
146 if (!Objects.equals(dto.getScope().name(), def.scope().name())) {
147 dto.setScope(RuleDto.Scope.valueOf(def.scope().name()));
150 if (!Objects.equals(dto.getLanguage(), def.repository().language())) {
151 dto.setLanguage(def.repository().language());
154 RuleType type = RuleType.valueOf(def.type().name());
155 if (!Objects.equals(dto.getType(), type.getDbConstant())) {
159 changed |= mergeCleanCodeAttribute(def, dto);
160 changed |= mergeImpacts(def, dto, uuidFactory);
162 dto.setIsAdHoc(false);
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);
175 // apply non-nullable default
176 if (dto.getCleanCodeAttribute() == null) {
177 dto.setCleanCodeAttribute(CleanCodeAttribute.defaultCleanCodeAttribute());
183 boolean mergeImpacts(RulesDefinition.Rule def, RuleDto dto, UuidFactory uuidFactory) {
184 if (dto.getEnumType() == RuleType.SECURITY_HOTSPOT) {
188 Map<SoftwareQuality, Severity> impactsFromPlugin = def.defaultImpacts();
189 Map<SoftwareQuality, Severity> impactsFromDb = dto.getDefaultImpacts().stream().collect(Collectors.toMap(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity));
191 if (impactsFromPlugin.isEmpty()) {
192 throw new IllegalStateException("There should be at least one impact defined for the rule " + def.key());
195 if (!Objects.equals(impactsFromDb, impactsFromPlugin)) {
196 dto.replaceAllDefaultImpacts(impactsFromPlugin.entrySet()
198 .map(e -> new ImpactDto().setUuid(uuidFactory.create()).setSoftwareQuality(e.getKey()).setSeverity(e.getValue()))
199 .collect(Collectors.toSet()));
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());
216 private static boolean mergeTags(RulesDefinition.Rule ruleDef, RuleDto dto) {
217 boolean changed = false;
219 if (RuleStatus.REMOVED == ruleDef.status()) {
220 dto.setSystemTags(emptySet());
222 } else if (dto.getSystemTags().size() != ruleDef.tags().size() ||
223 !dto.getSystemTags().containsAll(ruleDef.tags())) {
224 dto.setSystemTags(ruleDef.tags());
230 private static boolean mergeSecurityStandards(RulesDefinition.Rule ruleDef, RuleDto dto) {
231 boolean changed = false;
233 if (RuleStatus.REMOVED == ruleDef.status()) {
234 dto.setSecurityStandards(emptySet());
236 } else if (dto.getSecurityStandards().size() != ruleDef.securityStandards().size() ||
237 !dto.getSecurityStandards().containsAll(ruleDef.securityStandards())) {
238 dto.setSecurityStandards(ruleDef.securityStandards());
244 private static boolean containsHtmlDescription(RulesDefinition.Rule rule) {
245 return isNotEmpty(rule.htmlDescription()) || !rule.ruleDescriptionSections().isEmpty();
248 private static boolean ruleDescriptionSectionsUnchanged(RuleDto ruleDto, Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos) {
249 if (ruleDto.getRuleDescriptionSectionDtos().size() != newRuleDescriptionSectionDtos.size()) {
252 return ruleDto.getRuleDescriptionSectionDtos().stream()
253 .allMatch(sectionDto -> contains(newRuleDescriptionSectionDtos, sectionDto));
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()));
262 private static boolean mergeDebtDefinitions(RuleDto dto, @Nullable String remediationFunction,
263 @Nullable String remediationCoefficient, @Nullable String remediationOffset, @Nullable String gapDescription) {
264 boolean changed = false;
266 if (!Objects.equals(dto.getDefRemediationFunction(), remediationFunction)) {
267 dto.setDefRemediationFunction(remediationFunction);
270 if (!Objects.equals(dto.getDefRemediationGapMultiplier(), remediationCoefficient)) {
271 dto.setDefRemediationGapMultiplier(remediationCoefficient);
274 if (!Objects.equals(dto.getDefRemediationBaseEffort(), remediationOffset)) {
275 dto.setDefRemediationBaseEffort(remediationOffset);
278 if (!Objects.equals(dto.getGapDescription(), gapDescription)) {
279 dto.setGapDescription(gapDescription);
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;
290 return mergeDebtDefinitions(dto,
291 debtRemediationFunction.type().name(),
292 debtRemediationFunction.gapMultiplier(),
293 debtRemediationFunction.baseEffort(),
294 def.gapDescription());
296 return mergeDebtDefinitions(dto, null, null, null, null);
299 private boolean mergeDescription(RulesDefinition.Rule rule, RuleDto ruleDto) {
300 Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos = sectionsGeneratorResolver.generateFor(rule);
301 if (ruleDescriptionSectionsUnchanged(ruleDto, newRuleDescriptionSectionDtos)) {
304 ruleDto.replaceRuleDescriptionSectionDtos(newRuleDescriptionSectionDtos);
305 if (containsHtmlDescription(rule)) {
306 ruleDto.setDescriptionFormat(RuleDto.Format.HTML);
308 } else if (isNotEmpty(rule.markdownDescription())) {
309 ruleDto.setDescriptionFormat(RuleDto.Format.MARKDOWN);
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<>();
319 Profiler profiler = Profiler.create(LOG);
320 for (RuleParamDto paramDto : paramDtos) {
321 RulesDefinition.Param paramDef = ruleDef.param(paramDto.getName());
322 if (paramDef == null) {
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());
328 if (mergeParam(paramDto, paramDef)) {
329 dbClient.ruleDao().updateRuleParam(session, rule, paramDto);
331 existingParamsByName.put(paramDto.getName(), paramDto);
335 // Create newly parameters
336 for (RulesDefinition.Param param : ruleDef.params()) {
337 RuleParamDto paramDto = existingParamsByName.get(param.key());
338 if (paramDto != null) {
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())) {
350 // Propagate the default value to existing active rule parameters
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);
356 profiler.stopDebug(format("Propagate new param with name %s to active rules of rule %s", paramDto.getName(), rule.getKey()));
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());
366 if (!Objects.equals(paramDto.getDefaultValue(), paramDef.defaultValue())) {
367 paramDto.setDefaultValue(paramDef.defaultValue());
370 if (!Objects.equals(paramDto.getDescription(), paramDef.description())) {
371 paramDto.setDescription(paramDef.description());