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.ObjectUtils;
9 import org.apache.commons.lang.StringUtils;
10 import org.codehaus.plexus.util.StringInputStream;
11 import org.sonar.api.batch.Decorator;
12 import org.sonar.api.batch.DecoratorBarriers;
13 import org.sonar.api.batch.DecoratorContext;
14 import org.sonar.api.batch.DependsUpon;
15 import org.sonar.api.database.model.RuleFailureModel;
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 List<String> checksums = Lists.newArrayList();
44 public ViolationPersisterDecorator(RuleFinder ruleFinder, PastViolationsLoader pastViolationsLoader, ViolationPersister violationPersister) {
45 this.ruleFinder = ruleFinder;
46 this.pastViolationsLoader = pastViolationsLoader;
47 this.violationPersister = violationPersister;
50 public boolean shouldExecuteOnProject(Project project) {
54 public void decorate(Resource resource, DecoratorContext context) {
55 if (context.getViolations().isEmpty()) {
58 // Load past violations
59 List<RuleFailureModel> pastViolations = pastViolationsLoader.getPastViolations(resource);
60 // Load current source and calculate checksums
61 checksums = getChecksums(pastViolationsLoader.getSource(resource));
63 compareWithPastViolations(context, pastViolations);
68 private void compareWithPastViolations(DecoratorContext context, List<RuleFailureModel> pastViolations) {
69 Multimap<Rule, RuleFailureModel> pastViolationsByRule = LinkedHashMultimap.create();
70 for (RuleFailureModel pastViolation : pastViolations) {
71 Rule rule = ruleFinder.findById(pastViolation.getRuleId());
72 pastViolationsByRule.put(rule, pastViolation);
74 // for each violation, search equivalent past violation
75 for (Violation violation : context.getViolations()) {
76 RuleFailureModel pastViolation = selectPastViolation(violation, pastViolationsByRule);
77 if (pastViolation != null) {
78 // remove violation from past, since would be updated and shouldn't affect other violations anymore
79 pastViolationsByRule.remove(violation.getRule(), pastViolation);
81 String checksum = getChecksumForLine(checksums, violation.getLineId());
82 violationPersister.saveOrUpdateViolation(context.getProject(), violation, pastViolation, checksum);
87 * @return checksums, never null
89 private List<String> getChecksums(SnapshotSource source) {
90 return source == null || source.getData() == null ? Collections.<String> emptyList() : getChecksums(source.getData());
93 static List<String> getChecksums(String data) {
94 List<String> result = Lists.newArrayList();
96 List<String> lines = IOUtils.readLines(new StringInputStream(data));
97 for (String line : lines) {
98 result.add(getChecksum(line));
100 } catch (IOException e) {
101 throw new SonarException("Unable to calculate checksums", e);
106 static String getChecksum(String line) {
107 String reducedLine = StringUtils.replaceChars(line, SPACE_CHARS, "");
108 return DigestUtils.md5Hex(reducedLine);
112 * @return checksum or null if checksum not exists for line
114 private String getChecksumForLine(List<String> checksums, Integer line) {
115 if (line == null || line < 1 || line > checksums.size()) {
118 return checksums.get(line - 1);
122 * Search for past violation.
124 RuleFailureModel selectPastViolation(Violation violation, Multimap<Rule, RuleFailureModel> pastViolationsByRule) {
125 // skip violation, if there is no past violations with same rule
126 if (!pastViolationsByRule.containsKey(violation.getRule())) {
129 Collection<RuleFailureModel> pastViolations = pastViolationsByRule.get(violation.getRule());
130 RuleFailureModel found = selectPastViolationUsingLine(violation, pastViolations);
132 found = selectPastViolationUsingChecksum(violation, pastViolations);
138 * Search for past violation with same message and line.
140 private RuleFailureModel selectPastViolationUsingLine(Violation violation, Collection<RuleFailureModel> pastViolations) {
141 for (RuleFailureModel pastViolation : pastViolations) {
142 if (ObjectUtils.equals(violation.getLineId(), pastViolation.getLine()) && StringUtils.equals(violation.getMessage(), pastViolation.getMessage())) {
143 return pastViolation;
150 * Search for past violation with same message and checksum.
152 private RuleFailureModel selectPastViolationUsingChecksum(Violation violation, Collection<RuleFailureModel> pastViolations) {
153 String checksum = getChecksumForLine(checksums, violation.getLineId());
154 // skip violation, which not attached to line
155 if (checksum == null) {
158 for (RuleFailureModel pastViolation : pastViolations) {
159 String pastChecksum = pastViolation.getChecksum();
160 if (StringUtils.equals(checksum, pastChecksum) && StringUtils.equals(violation.getMessage(), pastViolation.getMessage())) {
161 return pastViolation;
168 public String toString() {
169 return getClass().getSimpleName();