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