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.qualityprofile.builtin;
22 import com.google.common.base.Splitter;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Date;
26 import java.util.List;
29 import java.util.stream.Stream;
30 import javax.annotation.CheckForNull;
31 import javax.annotation.Nullable;
32 import org.apache.commons.lang.StringUtils;
33 import org.sonar.api.rule.RuleStatus;
34 import org.sonar.api.server.ServerSide;
35 import org.sonar.api.server.rule.RuleParamType;
36 import org.sonar.api.utils.System2;
37 import org.sonar.core.util.stream.MoreCollectors;
38 import org.sonar.db.DbClient;
39 import org.sonar.db.DbSession;
40 import org.sonar.db.qualityprofile.ActiveRuleDao;
41 import org.sonar.db.qualityprofile.ActiveRuleDto;
42 import org.sonar.db.qualityprofile.ActiveRuleKey;
43 import org.sonar.db.qualityprofile.ActiveRuleParamDto;
44 import org.sonar.db.qualityprofile.OrgQProfileDto;
45 import org.sonar.db.qualityprofile.QProfileDto;
46 import org.sonar.db.qualityprofile.RulesProfileDto;
47 import org.sonar.db.rule.RuleDto;
48 import org.sonar.db.rule.RuleParamDto;
49 import org.sonar.server.qualityprofile.ActiveRuleChange;
50 import org.sonar.server.qualityprofile.ActiveRuleInheritance;
51 import org.sonar.server.qualityprofile.RuleActivation;
52 import org.sonar.server.qualityprofile.builtin.RuleActivationContext.ActiveRuleWrapper;
53 import org.sonar.server.qualityprofile.builtin.RuleActivationContext.RuleWrapper;
54 import org.sonar.server.user.UserSession;
55 import org.sonar.server.util.TypeValidations;
57 import static com.google.common.base.Preconditions.checkArgument;
58 import static java.util.stream.Collectors.toList;
59 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
62 * Activation and deactivation of rules in Quality profiles
65 public class RuleActivator {
67 private final System2 system2;
68 private final DbClient db;
69 private final TypeValidations typeValidations;
70 private final UserSession userSession;
72 public RuleActivator(System2 system2, DbClient db, TypeValidations typeValidations, UserSession userSession) {
73 this.system2 = system2;
75 this.typeValidations = typeValidations;
76 this.userSession = userSession;
80 public List<ActiveRuleChange> activate(DbSession dbSession, Collection<RuleActivation> activations, RuleActivationContext context) {
81 return activations.stream().map(a -> activate(dbSession, a, context))
82 .flatMap(List::stream)
86 public List<ActiveRuleChange> activate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
87 context.reset(activation.getRuleUuid());
88 return doActivate(dbSession, activation, context);
91 private List<ActiveRuleChange> doActivate(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
92 RuleDto rule = context.getRule().get();
93 checkRequest(RuleStatus.REMOVED != rule.getStatus(), "Rule was removed: %s", rule.getKey());
94 checkRequest(!rule.isTemplate(), "Rule template can't be activated on a Quality profile: %s", rule.getKey());
95 checkRequest(context.getRulesProfile().getLanguage().equals(rule.getLanguage()),
96 "%s rule %s cannot be activated on %s profile %s", rule.getLanguage(), rule.getKey(), context.getRulesProfile().getLanguage(), context.getRulesProfile().getName());
97 List<ActiveRuleChange> changes = new ArrayList<>();
98 ActiveRuleChange change;
99 boolean stopCascading = false;
101 ActiveRuleWrapper activeRule = context.getActiveRule();
102 ActiveRuleKey activeRuleKey = ActiveRuleKey.of(context.getRulesProfile(), rule.getKey());
103 if (activeRule == null) {
104 if (activation.isReset()) {
105 // ignore reset when rule is not activated
108 change = handleNewRuleActivation(activation, context, rule, activeRuleKey);
111 if (context.isCascading() && activeRule.get().doesOverride()) {
112 // propagating to descendants, but child profile already overrides rule -> stop propagation
115 change = new ActiveRuleChange(ActiveRuleChange.Type.UPDATED, activeRuleKey, rule);
116 stopCascading = handleUpdatedRuleActivation(activation, context, change, stopCascading, activeRule);
118 if (isSame(change, activeRule)) {
120 stopCascading = true;
124 if (change != null) {
126 persist(change, context, dbSession);
129 if (!changes.isEmpty()) {
130 updateProfileDates(dbSession, context);
133 if (!stopCascading) {
134 changes.addAll(propagateActivationToDescendants(dbSession, activation, context));
140 private boolean handleUpdatedRuleActivation(RuleActivation activation, RuleActivationContext context, ActiveRuleChange change,
141 boolean stopCascading, ActiveRuleWrapper activeRule) {
142 if (context.isCascading() && activeRule.get().getInheritance() == null) {
143 // activate on child, then on parent -> mark child as overriding parent
144 change.setInheritance(ActiveRuleInheritance.OVERRIDES);
145 change.setSeverity(activeRule.get().getSeverityString());
146 for (ActiveRuleParamDto activeParam : activeRule.getParams()) {
147 change.setParameter(activeParam.getKey(), activeParam.getValue());
149 stopCascading = true;
151 applySeverityAndParamToChange(activation, context, change);
152 if (!context.isCascading() && context.getParentActiveRule() != null) {
153 // override rule which is already declared on parents
154 change.setInheritance(isSameAsParent(change, context) ? ActiveRuleInheritance.INHERITED : ActiveRuleInheritance.OVERRIDES);
157 return stopCascading;
160 private ActiveRuleChange handleNewRuleActivation(RuleActivation activation, RuleActivationContext context, RuleDto rule, ActiveRuleKey activeRuleKey) {
161 ActiveRuleChange change = new ActiveRuleChange(ActiveRuleChange.Type.ACTIVATED, activeRuleKey, rule);
162 applySeverityAndParamToChange(activation, context, change);
163 if (context.isCascading() || isSameAsParent(change, context)) {
164 change.setInheritance(ActiveRuleInheritance.INHERITED);
169 private void updateProfileDates(DbSession dbSession, RuleActivationContext context) {
170 RulesProfileDto ruleProfile = context.getRulesProfile();
171 ruleProfile.setRulesUpdatedAtAsDate(new Date(context.getDate()));
172 db.qualityProfileDao().update(dbSession, ruleProfile);
174 if (userSession.isLoggedIn()) {
175 context.getProfiles().forEach(p -> db.qualityProfileDao().update(dbSession, OrgQProfileDto.from(p).setUserUpdatedAt(context.getDate())));
180 * Update severity and params
182 private void applySeverityAndParamToChange(RuleActivation request, RuleActivationContext context, ActiveRuleChange change) {
183 RuleWrapper rule = context.getRule();
184 ActiveRuleWrapper activeRule = context.getActiveRule();
185 ActiveRuleWrapper parentActiveRule = context.getParentActiveRule();
187 if (request.isReset()) {
188 applySeverityAndParamsWhenResetRequested(change, rule, parentActiveRule);
189 } else if (context.getRulesProfile().isBuiltIn()) {
190 applySeverityAndParamsWhenBuiltInProfile(request, context, change, rule);
192 applySeverityAndParamsWhenNonBuiltInProfile(request, context, change, rule, activeRule, parentActiveRule);
196 private void applySeverityAndParamsWhenResetRequested(ActiveRuleChange change, RuleWrapper rule, @Nullable ActiveRuleWrapper parentActiveRule) {
197 String severity = firstNonNull(
198 parentActiveRule != null ? parentActiveRule.get().getSeverityString() : null,
199 rule.get().getSeverityString());
200 change.setSeverity(severity);
202 for (RuleParamDto ruleParamDto : rule.getParams()) {
203 String paramKey = ruleParamDto.getName();
204 // load params from parent profile, else from default values
205 String paramValue = firstNonNull(
206 parentActiveRule != null ? parentActiveRule.getParamValue(paramKey) : null,
207 rule.getParamDefaultValue(paramKey));
209 change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
213 private void applySeverityAndParamsWhenBuiltInProfile(RuleActivation request, RuleActivationContext context, ActiveRuleChange change,
215 // for builtin quality profiles, the severity from profile, when null use the default severity of the rule
216 String severity = firstNonNull(request.getSeverity(), rule.get().getSeverityString());
217 change.setSeverity(severity);
219 for (RuleParamDto ruleParamDto : rule.getParams()) {
220 String paramKey = ruleParamDto.getName();
221 // use the value defined in the profile definition, else the rule default value
222 String paramValue = firstNonNull(
223 context.getRequestedParamValue(request, paramKey),
224 rule.getParamDefaultValue(paramKey));
225 change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
230 * 1. apply requested severity and param
231 * 2. if rule activated and overridden - apply user value
232 * 3. apply parent value
235 private void applySeverityAndParamsWhenNonBuiltInProfile(RuleActivation request, RuleActivationContext context, ActiveRuleChange change,
236 RuleWrapper rule, @Nullable ActiveRuleWrapper activeRule, @Nullable ActiveRuleWrapper parentActiveRule) {
237 String severity = getSeverityForNonBuiltInProfile(request, rule, activeRule, parentActiveRule);
238 change.setSeverity(severity);
240 for (RuleParamDto ruleParamDto : rule.getParams()) {
241 String paramKey = ruleParamDto.getName();
242 String parentValue = parentActiveRule != null ? parentActiveRule.getParamValue(paramKey) : null;
244 if (context.hasRequestedParamValue(request, paramKey)) {
245 // If the request contains the parameter then we're using either value from request, or parent value, or default value
246 paramValue = firstNonNull(
247 context.getRequestedParamValue(request, paramKey),
249 rule.getParamDefaultValue(paramKey));
250 } else if (activeRule != null) {
251 // If the request doesn't contain the parameter, then we're using either user value from db, or parent value if rule inherited, or default
253 paramValue = firstNonNull(
254 activeRule.get().doesOverride() ? activeRule.getParamValue(paramKey) : null,
255 parentValue == null ? activeRule.getParamValue(paramKey) : parentValue,
256 rule.getParamDefaultValue(paramKey));
258 paramValue = firstNonNull(
260 rule.getParamDefaultValue(paramKey));
262 change.setParameter(paramKey, validateParam(ruleParamDto, paramValue));
266 private static String getSeverityForNonBuiltInProfile(RuleActivation request, RuleWrapper rule, @Nullable ActiveRuleWrapper activeRule,
267 @Nullable ActiveRuleWrapper parentActiveRule) {
269 if (activeRule != null) {
270 ActiveRuleDto activeRuleDto = activeRule.get();
271 // load severity from request, else keep existing one (if overridden), else from parent if rule inherited, else from default
272 severity = firstNonNull(
273 request.getSeverity(),
274 activeRuleDto.doesOverride() ? activeRuleDto.getSeverityString() : null,
275 parentActiveRule != null ? parentActiveRule.get().getSeverityString() : activeRuleDto.getSeverityString(),
276 rule.get().getSeverityString());
278 // load severity from request, else from parent, else from default
279 severity = firstNonNull(
280 request.getSeverity(),
281 parentActiveRule != null ? parentActiveRule.get().getSeverityString() : null,
282 rule.get().getSeverityString());
287 private List<ActiveRuleChange> propagateActivationToDescendants(DbSession dbSession, RuleActivation activation, RuleActivationContext context) {
288 List<ActiveRuleChange> changes = new ArrayList<>();
290 // get all inherited profiles
291 context.getChildProfiles().forEach(child -> {
292 context.selectChild(child);
293 changes.addAll(doActivate(dbSession, activation, context));
298 private void persist(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
299 ActiveRuleDto activeRule = null;
300 if (change.getType() == ActiveRuleChange.Type.ACTIVATED) {
301 activeRule = doInsert(change, context, dbSession);
302 } else if (change.getType() == ActiveRuleChange.Type.DEACTIVATED) {
303 ActiveRuleDao dao = db.activeRuleDao();
304 activeRule = dao.delete(dbSession, change.getKey()).orElse(null);
306 } else if (change.getType() == ActiveRuleChange.Type.UPDATED) {
307 activeRule = doUpdate(change, context, dbSession);
309 change.setActiveRule(activeRule);
310 db.qProfileChangeDao().insert(dbSession, change.toDto(userSession.getUuid()));
313 private ActiveRuleDto doInsert(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
314 ActiveRuleDao dao = db.activeRuleDao();
315 RuleWrapper rule = context.getRule();
317 ActiveRuleDto activeRule = new ActiveRuleDto();
318 activeRule.setProfileUuid(context.getRulesProfile().getUuid());
319 activeRule.setRuleUuid(rule.get().getUuid());
320 activeRule.setKey(ActiveRuleKey.of(context.getRulesProfile(), rule.get().getKey()));
321 String severity = change.getSeverity();
322 if (severity != null) {
323 activeRule.setSeverity(severity);
325 ActiveRuleInheritance inheritance = change.getInheritance();
326 if (inheritance != null) {
327 activeRule.setInheritance(inheritance.name());
329 activeRule.setUpdatedAt(system2.now());
330 activeRule.setCreatedAt(system2.now());
331 dao.insert(dbSession, activeRule);
332 for (Map.Entry<String, String> param : change.getParameters().entrySet()) {
333 if (param.getValue() != null) {
334 ActiveRuleParamDto paramDto = ActiveRuleParamDto.createFor(rule.getParam(param.getKey()));
335 paramDto.setValue(param.getValue());
336 dao.insertParam(dbSession, activeRule, paramDto);
342 private ActiveRuleDto doUpdate(ActiveRuleChange change, RuleActivationContext context, DbSession dbSession) {
343 ActiveRuleWrapper activeRule = context.getActiveRule();
344 if (activeRule == null) {
347 ActiveRuleDao dao = db.activeRuleDao();
348 String severity = change.getSeverity();
349 if (severity != null) {
350 activeRule.get().setSeverity(severity);
352 ActiveRuleInheritance inheritance = change.getInheritance();
353 if (inheritance != null) {
354 activeRule.get().setInheritance(inheritance.name());
356 activeRule.get().setUpdatedAt(system2.now());
357 dao.update(dbSession, activeRule.get());
359 for (Map.Entry<String, String> param : change.getParameters().entrySet()) {
360 ActiveRuleParamDto activeRuleParamDto = activeRule.getParam(param.getKey());
361 if (activeRuleParamDto == null) {
363 if (param.getValue() != null) {
364 activeRuleParamDto = ActiveRuleParamDto.createFor(context.getRule().getParam(param.getKey()));
365 activeRuleParamDto.setValue(param.getValue());
366 dao.insertParam(dbSession, activeRule.get(), activeRuleParamDto);
369 if (param.getValue() != null) {
370 activeRuleParamDto.setValue(param.getValue());
371 dao.updateParam(dbSession, activeRuleParamDto);
373 dao.deleteParam(dbSession, activeRuleParamDto);
377 return activeRule.get();
380 public List<ActiveRuleChange> deactivate(DbSession dbSession, RuleActivationContext context, String ruleUuid, boolean force) {
381 context.reset(ruleUuid);
382 return doDeactivate(dbSession, context, force);
385 private List<ActiveRuleChange> doDeactivate(DbSession dbSession, RuleActivationContext context, boolean force) {
386 List<ActiveRuleChange> changes = new ArrayList<>();
387 ActiveRuleWrapper activeRule = context.getActiveRule();
388 if (activeRule == null) {
392 ActiveRuleChange change;
393 checkRequest(force || context.isCascading() || activeRule.get().getInheritance() == null, "Cannot deactivate inherited rule '%s'", context.getRule().get().getKey());
394 change = new ActiveRuleChange(ActiveRuleChange.Type.DEACTIVATED, activeRule.get(), context.getRule().get());
396 persist(change, context, dbSession);
398 // get all inherited profiles (they are not built-in by design)
399 context.getChildProfiles().forEach(child -> {
400 context.selectChild(child);
401 changes.addAll(doDeactivate(dbSession, context, force));
404 if (!changes.isEmpty()) {
405 updateProfileDates(dbSession, context);
412 private String validateParam(RuleParamDto ruleParam, @Nullable String value) {
414 RuleParamType ruleParamType = RuleParamType.parse(ruleParam.getType());
415 if (ruleParamType.multiple()) {
416 List<String> values = Splitter.on(",").splitToList(value);
417 typeValidations.validate(values, ruleParamType.type(), ruleParamType.values());
419 typeValidations.validate(value, ruleParamType.type(), ruleParamType.values());
425 public RuleActivationContext createContextForBuiltInProfile(DbSession dbSession, RulesProfileDto builtInProfile, Collection<String> ruleUuids) {
426 checkArgument(builtInProfile.isBuiltIn(), "Rules profile with UUID %s is not built-in", builtInProfile.getUuid());
428 RuleActivationContext.Builder builder = new RuleActivationContext.Builder();
429 builder.setDescendantProfilesSupplier(createDescendantProfilesSupplier(dbSession));
432 completeWithRules(dbSession, builder, ruleUuids);
434 // load org profiles. Their parents are null by nature.
435 List<QProfileDto> profiles = db.qualityProfileDao().selectQProfilesByRuleProfile(dbSession, builtInProfile);
436 builder.setProfiles(profiles);
437 builder.setBaseProfile(builtInProfile);
440 Collection<String> ruleProfileUuids = Stream
441 .concat(Stream.of(builtInProfile.getUuid()), profiles.stream().map(QProfileDto::getRulesProfileUuid))
442 .collect(MoreCollectors.toHashSet(profiles.size() + 1));
443 completeWithActiveRules(dbSession, builder, ruleUuids, ruleProfileUuids);
444 return builder.build();
447 public RuleActivationContext createContextForUserProfile(DbSession dbSession, QProfileDto profile, Collection<String> ruleUuids) {
448 checkArgument(!profile.isBuiltIn(), "Profile with UUID %s is built-in", profile.getKee());
449 RuleActivationContext.Builder builder = new RuleActivationContext.Builder();
450 builder.setDescendantProfilesSupplier(createDescendantProfilesSupplier(dbSession));
453 completeWithRules(dbSession, builder, ruleUuids);
456 List<QProfileDto> profiles = new ArrayList<>();
457 profiles.add(profile);
458 if (profile.getParentKee() != null) {
459 profiles.add(db.qualityProfileDao().selectByUuid(dbSession, profile.getParentKee()));
461 builder.setProfiles(profiles);
462 builder.setBaseProfile(RulesProfileDto.from(profile));
465 Collection<String> ruleProfileUuids = profiles.stream()
466 .map(QProfileDto::getRulesProfileUuid)
467 .collect(MoreCollectors.toHashSet(profiles.size()));
468 completeWithActiveRules(dbSession, builder, ruleUuids, ruleProfileUuids);
470 return builder.build();
473 DescendantProfilesSupplier createDescendantProfilesSupplier(DbSession dbSession) {
474 return (parents, ruleUuids) -> {
475 Collection<QProfileDto> profiles = db.qualityProfileDao().selectDescendants(dbSession, parents);
476 Set<String> ruleProfileUuids = profiles.stream()
477 .map(QProfileDto::getRulesProfileUuid)
478 .collect(MoreCollectors.toHashSet());
479 Collection<ActiveRuleDto> activeRules = db.activeRuleDao().selectByRulesAndRuleProfileUuids(dbSession, ruleUuids, ruleProfileUuids);
480 List<String> activeRuleUuids = activeRules.stream().map(ActiveRuleDto::getUuid).collect(MoreCollectors.toArrayList(activeRules.size()));
481 List<ActiveRuleParamDto> activeRuleParams = db.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, activeRuleUuids);
482 return new DescendantProfilesSupplier.Result(profiles, activeRules, activeRuleParams);
486 private void completeWithRules(DbSession dbSession, RuleActivationContext.Builder builder, Collection<String> ruleUuids) {
487 List<RuleDto> rules = db.ruleDao().selectByUuids(dbSession, ruleUuids);
488 builder.setRules(rules);
489 builder.setRuleParams(db.ruleDao().selectRuleParamsByRuleUuids(dbSession, ruleUuids));
492 private void completeWithActiveRules(DbSession dbSession, RuleActivationContext.Builder builder, Collection<String> ruleUuids, Collection<String> ruleProfileUuids) {
493 Collection<ActiveRuleDto> activeRules = db.activeRuleDao().selectByRulesAndRuleProfileUuids(dbSession, ruleUuids, ruleProfileUuids);
494 builder.setActiveRules(activeRules);
495 List<String> activeRuleUuids = activeRules.stream().map(ActiveRuleDto::getUuid).collect(MoreCollectors.toArrayList(activeRules.size()));
496 builder.setActiveRuleParams(db.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, activeRuleUuids));
499 private static boolean isSame(ActiveRuleChange change, ActiveRuleWrapper activeRule) {
500 ActiveRuleInheritance inheritance = change.getInheritance();
501 if (inheritance != null && !inheritance.name().equals(activeRule.get().getInheritance())) {
504 String severity = change.getSeverity();
505 if (severity != null && !severity.equals(activeRule.get().getSeverityString())) {
508 for (Map.Entry<String, String> changeParam : change.getParameters().entrySet()) {
509 String activeParamValue = activeRule.getParamValue(changeParam.getKey());
510 if (changeParam.getValue() == null && activeParamValue != null) {
513 if (changeParam.getValue() != null && (activeParamValue == null || !StringUtils.equals(changeParam.getValue(), activeParamValue))) {
521 * True if trying to override an inherited rule but with exactly the same values
523 private static boolean isSameAsParent(ActiveRuleChange change, RuleActivationContext context) {
524 ActiveRuleWrapper parentActiveRule = context.getParentActiveRule();
525 if (parentActiveRule == null) {
528 if (!StringUtils.equals(change.getSeverity(), parentActiveRule.get().getSeverityString())) {
531 for (Map.Entry<String, String> entry : change.getParameters().entrySet()) {
532 if (entry.getValue() != null && !entry.getValue().equals(parentActiveRule.getParamValue(entry.getKey()))) {
540 private static String firstNonNull(String... strings) {
541 for (String s : strings) {