You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RulesRegistrant.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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. import java.util.ArrayList;
  22. import java.util.Collection;
  23. import java.util.LinkedHashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. import java.util.Optional;
  28. import java.util.Set;
  29. import java.util.stream.Collectors;
  30. import java.util.stream.Stream;
  31. import org.sonar.api.Startable;
  32. import org.sonar.api.resources.Languages;
  33. import org.sonar.api.rule.RuleKey;
  34. import org.sonar.api.rule.RuleStatus;
  35. import org.sonar.api.server.rule.RulesDefinition;
  36. import org.sonar.api.utils.System2;
  37. import org.sonar.api.utils.log.Logger;
  38. import org.sonar.api.utils.log.Loggers;
  39. import org.sonar.api.utils.log.Profiler;
  40. import org.sonar.db.DbClient;
  41. import org.sonar.db.DbSession;
  42. import org.sonar.db.rule.RuleDto;
  43. import org.sonar.db.rule.RuleRepositoryDto;
  44. import org.sonar.server.es.metadata.MetadataIndex;
  45. import org.sonar.server.qualityprofile.ActiveRuleChange;
  46. import org.sonar.server.qualityprofile.QProfileRules;
  47. import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
  48. import org.sonar.server.rule.RuleDefinitionsLoader;
  49. import org.sonar.server.rule.WebServerRuleFinder;
  50. import org.sonar.server.rule.index.RuleIndexer;
  51. import static com.google.common.base.Preconditions.checkNotNull;
  52. import static java.lang.String.format;
  53. import static java.util.Collections.emptyList;
  54. import static java.util.Collections.emptySet;
  55. /**
  56. * Registers rules at server startup
  57. */
  58. public class RulesRegistrant implements Startable {
  59. private static final Logger LOG = Loggers.get(RulesRegistrant.class);
  60. private final RuleDefinitionsLoader defLoader;
  61. private final QProfileRules qProfileRules;
  62. private final DbClient dbClient;
  63. private final RuleIndexer ruleIndexer;
  64. private final ActiveRuleIndexer activeRuleIndexer;
  65. private final Languages languages;
  66. private final System2 system2;
  67. private final WebServerRuleFinder webServerRuleFinder;
  68. private final MetadataIndex metadataIndex;
  69. private final RulesKeyVerifier rulesKeyVerifier;
  70. private final StartupRuleUpdater startupRuleUpdater;
  71. private final NewRuleCreator newRuleCreator;
  72. public RulesRegistrant(RuleDefinitionsLoader defLoader, QProfileRules qProfileRules, DbClient dbClient, RuleIndexer ruleIndexer,
  73. ActiveRuleIndexer activeRuleIndexer, Languages languages, System2 system2, WebServerRuleFinder webServerRuleFinder,
  74. MetadataIndex metadataIndex, RulesKeyVerifier rulesKeyVerifier, StartupRuleUpdater startupRuleUpdater,
  75. NewRuleCreator newRuleCreator) {
  76. this.defLoader = defLoader;
  77. this.qProfileRules = qProfileRules;
  78. this.dbClient = dbClient;
  79. this.ruleIndexer = ruleIndexer;
  80. this.activeRuleIndexer = activeRuleIndexer;
  81. this.languages = languages;
  82. this.system2 = system2;
  83. this.webServerRuleFinder = webServerRuleFinder;
  84. this.metadataIndex = metadataIndex;
  85. this.rulesKeyVerifier = rulesKeyVerifier;
  86. this.startupRuleUpdater = startupRuleUpdater;
  87. this.newRuleCreator = newRuleCreator;
  88. }
  89. @Override
  90. public void start() {
  91. Profiler profiler = Profiler.create(LOG).startInfo("Register rules");
  92. try (DbSession dbSession = dbClient.openSession(true)) {
  93. List<RulesDefinition.Repository> repositories = defLoader.load().repositories();
  94. RulesRegistrationContext rulesRegistrationContext = RulesRegistrationContext.create(dbClient, dbSession);
  95. rulesKeyVerifier.verifyRuleKeyConsistency(repositories, rulesRegistrationContext);
  96. for (RulesDefinition.ExtendedRepository repoDef : repositories) {
  97. if (languages.get(repoDef.language()) != null) {
  98. registerRules(rulesRegistrationContext, repoDef.rules(), dbSession);
  99. dbSession.commit();
  100. }
  101. }
  102. processRemainingDbRules(rulesRegistrationContext, dbSession);
  103. List<ActiveRuleChange> changes = removeActiveRulesOnStillExistingRepositories(dbSession, rulesRegistrationContext, repositories);
  104. dbSession.commit();
  105. persistRepositories(dbSession, repositories);
  106. // FIXME lack of resiliency, active rules index is corrupted if rule index fails
  107. // to be updated. Only a single DB commit should be executed.
  108. ruleIndexer.commitAndIndex(dbSession, rulesRegistrationContext.getAllModified().map(RuleDto::getUuid).collect(Collectors.toSet()));
  109. changes.forEach(arChange -> dbClient.qProfileChangeDao().insert(dbSession, arChange.toDto(null)));
  110. activeRuleIndexer.commitAndIndex(dbSession, changes);
  111. rulesRegistrationContext.getRenamed().forEach(e -> LOG.info("Rule {} re-keyed to {}", e.getValue(), e.getKey().getKey()));
  112. profiler.stopDebug();
  113. if (!rulesRegistrationContext.hasDbRules()) {
  114. Stream.concat(ruleIndexer.getIndexTypes().stream(), activeRuleIndexer.getIndexTypes().stream())
  115. .forEach(t -> metadataIndex.setInitialized(t, true));
  116. }
  117. webServerRuleFinder.startCaching();
  118. }
  119. }
  120. private void persistRepositories(DbSession dbSession, List<RulesDefinition.Repository> repositories) {
  121. List<String> keys = repositories.stream().map(RulesDefinition.Repository::key).toList();
  122. Set<String> existingKeys = dbClient.ruleRepositoryDao().selectAllKeys(dbSession);
  123. Map<Boolean, List<RuleRepositoryDto>> dtos = repositories.stream()
  124. .map(r -> new RuleRepositoryDto(r.key(), r.language(), r.name()))
  125. .collect(Collectors.groupingBy(i -> existingKeys.contains(i.getKey())));
  126. dbClient.ruleRepositoryDao().update(dbSession, dtos.getOrDefault(true, emptyList()));
  127. dbClient.ruleRepositoryDao().insert(dbSession, dtos.getOrDefault(false, emptyList()));
  128. dbClient.ruleRepositoryDao().deleteIfKeyNotIn(dbSession, keys);
  129. dbSession.commit();
  130. }
  131. @Override
  132. public void stop() {
  133. // nothing
  134. }
  135. private void registerRules(RulesRegistrationContext context, List<RulesDefinition.Rule> ruleDefs, DbSession session) {
  136. Map<RulesDefinition.Rule, RuleDto> dtos = new LinkedHashMap<>(ruleDefs.size());
  137. for (RulesDefinition.Rule ruleDef : ruleDefs) {
  138. RuleKey ruleKey = RuleKey.of(ruleDef.repository().key(), ruleDef.key());
  139. RuleDto ruleDto = context.getDbRuleFor(ruleDef).orElseGet(() -> newRuleCreator.createNewRule(context, ruleDef, session));
  140. dtos.put(ruleDef, ruleDto);
  141. // we must detect renaming __before__ we modify the DTO
  142. if (!ruleDto.getKey().equals(ruleKey)) {
  143. context.renamed(ruleDto);
  144. ruleDto.setRuleKey(ruleKey);
  145. }
  146. if (!context.isCreated(ruleDto) && startupRuleUpdater.findChangesAndUpdateRule(ruleDef, ruleDto)) {
  147. context.updated(ruleDto);
  148. }
  149. if (context.isUpdated(ruleDto) || context.isRenamed(ruleDto)) {
  150. update(session, ruleDto);
  151. } else if (!context.isCreated(ruleDto)) {
  152. context.unchanged(ruleDto);
  153. }
  154. }
  155. for (Map.Entry<RulesDefinition.Rule, RuleDto> e : dtos.entrySet()) {
  156. startupRuleUpdater.mergeParams(context, e.getKey(), e.getValue(), session);
  157. startupRuleUpdater.updateDeprecatedKeys(context, e.getKey(), e.getValue(), session);
  158. }
  159. }
  160. private void processRemainingDbRules(RulesRegistrationContext recorder, DbSession dbSession) {
  161. // custom rules check status of template, so they must be processed at the end
  162. List<RuleDto> customRules = new ArrayList<>();
  163. recorder.getRemaining().forEach(rule -> {
  164. if (rule.isCustomRule()) {
  165. customRules.add(rule);
  166. } else if (!rule.isAdHoc() && rule.getStatus() != RuleStatus.REMOVED) {
  167. removeRule(dbSession, recorder, rule);
  168. }
  169. });
  170. for (RuleDto customRule : customRules) {
  171. String templateUuid = customRule.getTemplateUuid();
  172. checkNotNull(templateUuid, "Template uuid of the custom rule '%s' is null", customRule);
  173. Optional<RuleDto> template = dbClient.ruleDao().selectByUuid(templateUuid, dbSession);
  174. if (template.isPresent() && template.get().getStatus() != RuleStatus.REMOVED) {
  175. if (updateCustomRuleFromTemplateRule(customRule, template.get())) {
  176. recorder.updated(customRule);
  177. update(dbSession, customRule);
  178. }
  179. } else {
  180. removeRule(dbSession, recorder, customRule);
  181. }
  182. }
  183. dbSession.commit();
  184. }
  185. private void removeRule(DbSession session, RulesRegistrationContext recorder, RuleDto rule) {
  186. LOG.info(format("Disable rule %s", rule.getKey()));
  187. rule.setStatus(RuleStatus.REMOVED);
  188. rule.setSystemTags(emptySet());
  189. update(session, rule);
  190. // FIXME resetting the tags for all organizations must be handled a different way
  191. // rule.setTags(Collections.emptySet());
  192. // update(session, rule.getMetadata());
  193. recorder.removed(rule);
  194. if (recorder.getRemoved().count() % 100 == 0) {
  195. session.commit();
  196. }
  197. }
  198. private static boolean updateCustomRuleFromTemplateRule(RuleDto customRule, RuleDto templateRule) {
  199. boolean changed = false;
  200. if (!Objects.equals(customRule.getLanguage(), templateRule.getLanguage())) {
  201. customRule.setLanguage(templateRule.getLanguage());
  202. changed = true;
  203. }
  204. if (!Objects.equals(customRule.getConfigKey(), templateRule.getConfigKey())) {
  205. customRule.setConfigKey(templateRule.getConfigKey());
  206. changed = true;
  207. }
  208. if (!Objects.equals(customRule.getPluginKey(), templateRule.getPluginKey())) {
  209. customRule.setPluginKey(templateRule.getPluginKey());
  210. changed = true;
  211. }
  212. if (!Objects.equals(customRule.getDefRemediationFunction(), templateRule.getDefRemediationFunction())) {
  213. customRule.setDefRemediationFunction(templateRule.getDefRemediationFunction());
  214. changed = true;
  215. }
  216. if (!Objects.equals(customRule.getDefRemediationGapMultiplier(), templateRule.getDefRemediationGapMultiplier())) {
  217. customRule.setDefRemediationGapMultiplier(templateRule.getDefRemediationGapMultiplier());
  218. changed = true;
  219. }
  220. if (!Objects.equals(customRule.getDefRemediationBaseEffort(), templateRule.getDefRemediationBaseEffort())) {
  221. customRule.setDefRemediationBaseEffort(templateRule.getDefRemediationBaseEffort());
  222. changed = true;
  223. }
  224. if (!Objects.equals(customRule.getGapDescription(), templateRule.getGapDescription())) {
  225. customRule.setGapDescription(templateRule.getGapDescription());
  226. changed = true;
  227. }
  228. if (customRule.getStatus() != templateRule.getStatus()) {
  229. customRule.setStatus(templateRule.getStatus());
  230. changed = true;
  231. }
  232. if (!Objects.equals(customRule.getSeverityString(), templateRule.getSeverityString())) {
  233. customRule.setSeverity(templateRule.getSeverityString());
  234. changed = true;
  235. }
  236. if (!Objects.equals(customRule.getRepositoryKey(), templateRule.getRepositoryKey())) {
  237. customRule.setRepositoryKey(templateRule.getRepositoryKey());
  238. changed = true;
  239. }
  240. return changed;
  241. }
  242. /**
  243. * SONAR-4642
  244. * <p/>
  245. * Remove active rules on repositories that still exists.
  246. * <p/>
  247. * For instance, if the javascript repository do not provide anymore some rules, active rules related to this rules will be removed.
  248. * But if the javascript repository do not exists anymore, then related active rules will not be removed.
  249. * <p/>
  250. * The side effect of this approach is that extended repositories will not be managed the same way.
  251. * If an extended repository do not exists anymore, then related active rules will be removed.
  252. */
  253. private List<ActiveRuleChange> removeActiveRulesOnStillExistingRepositories(DbSession dbSession, RulesRegistrationContext recorder, List<RulesDefinition.Repository> context) {
  254. Set<String> existingAndRenamedRepositories = getExistingAndRenamedRepositories(recorder, context);
  255. List<ActiveRuleChange> changes = new ArrayList<>();
  256. Profiler profiler = Profiler.create(LOG);
  257. recorder.getRemoved()
  258. .filter(rule -> existingAndRenamedRepositories.contains(rule.getRepositoryKey()))
  259. .forEach(rule -> {
  260. // SONAR-4642 Remove active rules only when repository still exists
  261. profiler.start();
  262. changes.addAll(qProfileRules.deleteRule(dbSession, rule));
  263. profiler.stopDebug(format("Remove active rule for rule %s", rule.getKey()));
  264. });
  265. return changes;
  266. }
  267. private static Set<String> getExistingAndRenamedRepositories(RulesRegistrationContext recorder, Collection<RulesDefinition.Repository> context) {
  268. return Stream.concat(
  269. context.stream().map(RulesDefinition.ExtendedRepository::key),
  270. recorder.getRenamed().map(Map.Entry::getValue).map(RuleKey::repository))
  271. .collect(Collectors.toSet());
  272. }
  273. private void update(DbSession session, RuleDto rule) {
  274. rule.setUpdatedAt(system2.now());
  275. dbClient.ruleDao().update(session, rule);
  276. }
  277. }