Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

ComponentIssuesLoader.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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.ce.task.projectanalysis.issue;
  21. import java.time.temporal.ChronoUnit;
  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.Collections;
  25. import java.util.Date;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.Optional;
  29. import org.apache.ibatis.session.ResultContext;
  30. import org.apache.ibatis.session.ResultHandler;
  31. import org.sonar.api.config.Configuration;
  32. import org.sonar.api.rule.RuleKey;
  33. import org.sonar.api.rule.RuleStatus;
  34. import org.sonar.api.utils.System2;
  35. import org.sonar.api.utils.log.Loggers;
  36. import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder;
  37. import org.sonar.core.issue.DefaultIssue;
  38. import org.sonar.core.issue.FieldDiffs;
  39. import org.sonar.db.DbClient;
  40. import org.sonar.db.DbSession;
  41. import org.sonar.db.issue.IssueChangeDto;
  42. import org.sonar.db.issue.IssueDto;
  43. import org.sonar.db.issue.IssueMapper;
  44. import static com.google.common.base.Preconditions.checkState;
  45. import static java.util.Collections.emptyList;
  46. import static java.util.Collections.unmodifiableList;
  47. import static java.util.stream.Collectors.groupingBy;
  48. import static java.util.stream.Collectors.toList;
  49. import static org.sonar.api.issue.Issue.STATUS_CLOSED;
  50. import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
  51. public class ComponentIssuesLoader {
  52. private static final int DEFAULT_CLOSED_ISSUES_MAX_AGE = 30;
  53. private static final String PROPERTY_CLOSED_ISSUE_MAX_AGE = "sonar.issuetracking.closedissues.maxage";
  54. private final DbClient dbClient;
  55. private final RuleRepository ruleRepository;
  56. private final ActiveRulesHolder activeRulesHolder;
  57. private final System2 system2;
  58. private final int closedIssueMaxAge;
  59. public ComponentIssuesLoader(DbClient dbClient, RuleRepository ruleRepository, ActiveRulesHolder activeRulesHolder,
  60. Configuration configuration, System2 system2) {
  61. this.dbClient = dbClient;
  62. this.activeRulesHolder = activeRulesHolder;
  63. this.ruleRepository = ruleRepository;
  64. this.system2 = system2;
  65. this.closedIssueMaxAge = configuration.get(PROPERTY_CLOSED_ISSUE_MAX_AGE)
  66. .map(ComponentIssuesLoader::safelyParseClosedIssueMaxAge)
  67. .filter(i -> i >= 0)
  68. .orElse(DEFAULT_CLOSED_ISSUES_MAX_AGE);
  69. }
  70. private static Integer safelyParseClosedIssueMaxAge(String str) {
  71. try {
  72. return Integer.parseInt(str);
  73. } catch (NumberFormatException e) {
  74. Loggers.get(ComponentIssuesLoader.class)
  75. .warn("Value of property {} should be an integer >= 0: {}", PROPERTY_CLOSED_ISSUE_MAX_AGE, str);
  76. return null;
  77. }
  78. }
  79. public List<DefaultIssue> loadOpenIssues(String componentUuid) {
  80. try (DbSession dbSession = dbClient.openSession(false)) {
  81. return loadOpenIssues(componentUuid, dbSession);
  82. }
  83. }
  84. public List<DefaultIssue> loadOpenIssuesWithChanges(String componentUuid) {
  85. try (DbSession dbSession = dbClient.openSession(false)) {
  86. List<DefaultIssue> result = loadOpenIssues(componentUuid, dbSession);
  87. return loadChanges(dbSession, result);
  88. }
  89. }
  90. public List<DefaultIssue> loadChanges(DbSession dbSession, Collection<DefaultIssue> issues) {
  91. Map<String, List<IssueChangeDto>> changeDtoByIssueKey = dbClient.issueChangeDao()
  92. .selectByIssueKeys(dbSession, issues.stream().map(DefaultIssue::key).collect(toList()))
  93. .stream()
  94. .collect(groupingBy(IssueChangeDto::getIssueKey));
  95. issues.forEach(i -> setChanges(changeDtoByIssueKey, i));
  96. return new ArrayList<>(issues);
  97. }
  98. /**
  99. * Loads the most recent diff changes of the specified issues which contain the latest status and resolution of the
  100. * issue.
  101. */
  102. public void loadLatestDiffChangesForReopeningOfClosedIssues(Collection<DefaultIssue> issues) {
  103. if (issues.isEmpty()) {
  104. return;
  105. }
  106. try (DbSession dbSession = dbClient.openSession(false)) {
  107. loadLatestDiffChangesForReopeningOfClosedIssues(dbSession, issues);
  108. }
  109. }
  110. /**
  111. * To be efficient both in term of memory and speed:
  112. * <ul>
  113. * <li>only diff changes are loaded from DB, sorted by issue and then change creation date</li>
  114. * <li>data from DB is streamed</li>
  115. * <li>only the latest change(s) with status and resolution are added to the {@link DefaultIssue} objects</li>
  116. * </ul>
  117. */
  118. private void loadLatestDiffChangesForReopeningOfClosedIssues(DbSession dbSession, Collection<DefaultIssue> issues) {
  119. Map<String, DefaultIssue> issuesByKey = issues.stream().collect(uniqueIndex(DefaultIssue::key));
  120. dbClient.issueChangeDao()
  121. .scrollDiffChangesOfIssues(dbSession, issuesByKey.keySet(), new ResultHandler<IssueChangeDto>() {
  122. private DefaultIssue currentIssue = null;
  123. private boolean previousStatusFound = false;
  124. private boolean previousResolutionFound = false;
  125. @Override
  126. public void handleResult(ResultContext<? extends IssueChangeDto> resultContext) {
  127. IssueChangeDto issueChangeDto = resultContext.getResultObject();
  128. if (currentIssue == null || !currentIssue.key().equals(issueChangeDto.getIssueKey())) {
  129. currentIssue = issuesByKey.get(issueChangeDto.getIssueKey());
  130. previousStatusFound = false;
  131. previousResolutionFound = false;
  132. }
  133. if (currentIssue != null) {
  134. FieldDiffs fieldDiffs = issueChangeDto.toFieldDiffs();
  135. boolean hasPreviousStatus = fieldDiffs.get("status") != null;
  136. boolean hasPreviousResolution = fieldDiffs.get("resolution") != null;
  137. if ((!previousStatusFound && hasPreviousStatus) || (!previousResolutionFound && hasPreviousResolution)) {
  138. currentIssue.addChange(fieldDiffs);
  139. }
  140. previousStatusFound |= hasPreviousStatus;
  141. previousResolutionFound |= hasPreviousResolution;
  142. }
  143. }
  144. });
  145. }
  146. private List<DefaultIssue> loadOpenIssues(String componentUuid, DbSession dbSession) {
  147. List<DefaultIssue> result = new ArrayList<>();
  148. dbSession.getMapper(IssueMapper.class).scrollNonClosedByComponentUuid(componentUuid, resultContext -> {
  149. DefaultIssue issue = (resultContext.getResultObject()).toDefaultIssue();
  150. Rule rule = ruleRepository.getByKey(issue.ruleKey());
  151. // TODO this field should be set outside this class
  152. if ((!rule.isExternal() && !isActive(issue.ruleKey())) || rule.getStatus() == RuleStatus.REMOVED) {
  153. issue.setOnDisabledRule(true);
  154. // TODO to be improved, why setOnDisabledRule(true) is not enough ?
  155. issue.setBeingClosed(true);
  156. }
  157. issue.setSelectedAt(System.currentTimeMillis());
  158. result.add(issue);
  159. });
  160. return Collections.unmodifiableList(result);
  161. }
  162. private static void setChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) {
  163. changeDtoByIssueKey.computeIfAbsent(i.key(), k -> emptyList())
  164. .forEach(c -> addChangeOrComment(i, c));
  165. }
  166. private static void addChangeOrComment(DefaultIssue i, IssueChangeDto c) {
  167. switch (c.getChangeType()) {
  168. case IssueChangeDto.TYPE_FIELD_CHANGE:
  169. i.addChange(c.toFieldDiffs());
  170. break;
  171. case IssueChangeDto.TYPE_COMMENT:
  172. i.addComment(c.toComment());
  173. break;
  174. default:
  175. throw new IllegalStateException("Unknown change type: " + c.getChangeType());
  176. }
  177. }
  178. private boolean isActive(RuleKey ruleKey) {
  179. return activeRulesHolder.get(ruleKey).isPresent();
  180. }
  181. /**
  182. * Load closed issues for the specified Component, which have at least one line diff in changelog AND are
  183. * not manual vulnerabilities.
  184. * <p>
  185. * Closed issues do not have a line number in DB (it is unset when the issue is closed), this method
  186. * returns {@link DefaultIssue} objects which line number is populated from the most recent diff logging
  187. * the removal of the line. Closed issues which do not have such diff are not loaded.
  188. * <p>
  189. * To not depend on purge configuration of closed issues, only issues which close date is less than 30 days ago at
  190. * 00H00 are returned.
  191. */
  192. public List<DefaultIssue> loadClosedIssues(String componentUuid) {
  193. if (closedIssueMaxAge == 0) {
  194. return emptyList();
  195. }
  196. Date date = new Date(system2.now());
  197. long closeDateAfter = date.toInstant()
  198. .minus(closedIssueMaxAge, ChronoUnit.DAYS)
  199. .truncatedTo(ChronoUnit.DAYS)
  200. .toEpochMilli();
  201. try (DbSession dbSession = dbClient.openSession(false)) {
  202. return loadClosedIssues(dbSession, componentUuid, closeDateAfter);
  203. }
  204. }
  205. private static List<DefaultIssue> loadClosedIssues(DbSession dbSession, String componentUuid, long closeDateAfter) {
  206. ClosedIssuesResultHandler handler = new ClosedIssuesResultHandler();
  207. dbSession.getMapper(IssueMapper.class).scrollClosedByComponentUuid(componentUuid, closeDateAfter, handler);
  208. return unmodifiableList(handler.issues);
  209. }
  210. private static class ClosedIssuesResultHandler implements ResultHandler<IssueDto> {
  211. private final List<DefaultIssue> issues = new ArrayList<>();
  212. private String previousIssueKey = null;
  213. @Override
  214. public void handleResult(ResultContext<? extends IssueDto> resultContext) {
  215. IssueDto resultObject = resultContext.getResultObject();
  216. // issue are ordered by most recent change first, only the first row for a given issue is of interest
  217. if (previousIssueKey != null && previousIssueKey.equals(resultObject.getKey())) {
  218. return;
  219. }
  220. FieldDiffs fieldDiffs = FieldDiffs.parse(resultObject.getClosedChangeData()
  221. .orElseThrow(() -> new IllegalStateException("Close change data should be populated")));
  222. checkState(Optional.ofNullable(fieldDiffs.get("status"))
  223. .map(FieldDiffs.Diff::newValue)
  224. .filter(STATUS_CLOSED::equals)
  225. .isPresent(), "Close change data should have a status diff with new value %s", STATUS_CLOSED);
  226. Integer line = Optional.ofNullable(fieldDiffs.get("line"))
  227. .map(diff -> (String) diff.oldValue())
  228. .filter(str -> !str.isEmpty())
  229. .map(Integer::parseInt)
  230. .orElse(null);
  231. previousIssueKey = resultObject.getKey();
  232. DefaultIssue issue = resultObject.toDefaultIssue();
  233. issue.setLine(line);
  234. issue.setSelectedAt(System.currentTimeMillis());
  235. issues.add(issue);
  236. }
  237. }
  238. }