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