1 package org.sonar.plugins.core.timemachine;
3 import com.google.common.collect.LinkedHashMultimap;
4 import com.google.common.collect.Lists;
5 import com.google.common.collect.Multimap;
6 import org.apache.commons.codec.digest.DigestUtils;
7 import org.apache.commons.io.IOUtils;
8 import org.apache.commons.lang.StringUtils;
9 import org.codehaus.plexus.util.StringInputStream;
10 import org.sonar.api.batch.Decorator;
11 import org.sonar.api.batch.DecoratorBarriers;
12 import org.sonar.api.batch.DecoratorContext;
13 import org.sonar.api.batch.DependsUpon;
14 import org.sonar.api.database.model.RuleFailureModel;
15 import org.sonar.api.database.model.Snapshot;
16 import org.sonar.api.database.model.SnapshotSource;
17 import org.sonar.api.resources.Project;
18 import org.sonar.api.resources.Resource;
19 import org.sonar.api.rules.Rule;
20 import org.sonar.api.rules.RuleFinder;
21 import org.sonar.api.rules.Violation;
22 import org.sonar.api.utils.SonarException;
23 import org.sonar.batch.index.ViolationPersister;
25 import java.io.IOException;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.List;
30 @DependsUpon(DecoratorBarriers.END_OF_VIOLATIONS_GENERATION)
31 public class ViolationPersisterDecorator implements Decorator {
34 * Those chars would be ignored during generation of checksums.
36 private static final String SPACE_CHARS = "\t\n\r ";
38 private RuleFinder ruleFinder;
39 private PastViolationsLoader pastViolationsLoader;
40 private ViolationPersister violationPersister;
42 private List<String> checksums = Lists.newArrayList();
43 private List<String> pastChecksums = Lists.newArrayList();
45 public ViolationPersisterDecorator(RuleFinder ruleFinder, PastViolationsLoader pastViolationsLoader, ViolationPersister violationPersister) {
46 this.ruleFinder = ruleFinder;
47 this.pastViolationsLoader = pastViolationsLoader;
48 this.violationPersister = violationPersister;
51 public boolean shouldExecuteOnProject(Project project) {
55 public void decorate(Resource resource, DecoratorContext context) {
56 Snapshot previousLastSnapshot = pastViolationsLoader.getPreviousLastSnapshot(resource);
57 // Load past violations
58 List<RuleFailureModel> pastViolations = pastViolationsLoader.getPastViolations(previousLastSnapshot);
59 // Load past source and calculate checksums
60 pastChecksums = getChecksums(pastViolationsLoader.getPastSource(previousLastSnapshot));
61 // Load current source and calculate checksums
62 checksums = getChecksums(pastViolationsLoader.getSource(resource));
64 compareWithPastViolations(context, pastViolations);
67 pastChecksums.clear();
70 private void compareWithPastViolations(DecoratorContext context, List<RuleFailureModel> pastViolations) {
71 Multimap<Rule, RuleFailureModel> pastViolationsByRule = LinkedHashMultimap.create();
72 for (RuleFailureModel pastViolation : pastViolations) {
73 Rule rule = ruleFinder.findById(pastViolation.getRuleId());
74 pastViolationsByRule.put(rule, pastViolation);
76 // for each violation, search equivalent past violation
77 for (Violation violation : context.getViolations()) {
78 RuleFailureModel pastViolation = selectPastViolation(violation, pastViolationsByRule);
79 if (pastViolation != null) {
80 // remove violation from past, since would be updated and shouldn't affect other violations anymore
81 pastViolationsByRule.remove(violation.getRule(), pastViolation);
83 violationPersister.saveOrUpdateViolation(context.getProject(), violation, pastViolation);
88 * @return checksums, never null
90 private List<String> getChecksums(SnapshotSource source) {
91 return source == null || source.getData() == null ? Collections.<String> emptyList() : getChecksums(source.getData());
94 static List<String> getChecksums(String data) {
95 List<String> result = Lists.newArrayList();
97 List<String> lines = IOUtils.readLines(new StringInputStream(data));
98 for (String line : lines) {
99 String reducedLine = StringUtils.replaceChars(line, SPACE_CHARS, "");
100 result.add(DigestUtils.md5Hex(reducedLine));
102 } catch (IOException e) {
103 throw new SonarException("Unable to calculate checksums", e);
109 * @return checksum or null if checksum not exists for line
111 private String getChecksumForLine(List<String> checksums, Integer line) {
112 if (line == null || line < 1 || line > checksums.size()) {
115 return checksums.get(line - 1);
119 * Search for past violation.
121 RuleFailureModel selectPastViolation(Violation violation, Multimap<Rule, RuleFailureModel> pastViolationsByRule) {
122 // skip violation, if there is no past violations with same rule
123 if (!pastViolationsByRule.containsKey(violation.getRule())) {
126 Collection<RuleFailureModel> pastViolations = pastViolationsByRule.get(violation.getRule());
127 RuleFailureModel found = selectPastViolationUsingLine(violation, pastViolations);
129 found = selectPastViolationUsingChecksum(violation, pastViolations);
135 * Search for past violation with same message and line.
137 RuleFailureModel selectPastViolationUsingLine(Violation violation, Collection<RuleFailureModel> pastViolations) {
138 for (RuleFailureModel pastViolation : pastViolations) {
139 if (violation.getLineId() == pastViolation.getLine() && StringUtils.equals(violation.getMessage(), pastViolation.getMessage())) {
140 return pastViolation;
147 * Search for past violation with same message and checksum.
149 RuleFailureModel selectPastViolationUsingChecksum(Violation violation, Collection<RuleFailureModel> pastViolations) {
150 String checksum = getChecksumForLine(checksums, violation.getLineId());
151 // skip violation, which not attached to line
152 if (checksum == null) {
155 for (RuleFailureModel pastViolation : pastViolations) {
156 String pastChecksum = getChecksumForLine(pastChecksums, pastViolation.getLine());
157 if (StringUtils.equals(checksum, pastChecksum) && StringUtils.equals(violation.getMessage(), pastViolation.getMessage())) {
158 return pastViolation;
165 public String toString() {
166 return getClass().getSimpleName();