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.components.PastViolationsLoader;
24 import org.sonar.batch.index.ViolationPersister;
26 import java.io.IOException;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.List;
31 @DependsUpon(DecoratorBarriers.END_OF_VIOLATIONS_GENERATION)
32 public class ViolationPersisterDecorator implements Decorator {
35 * Those chars would be ignored during generation of checksums.
37 private static final String SPACE_CHARS = "\t\n\r ";
39 private RuleFinder ruleFinder;
40 private PastViolationsLoader pastViolationsLoader;
41 private ViolationPersister violationPersister;
43 List<String> checksums = 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 if (context.getViolations().isEmpty()) {
59 // Load past violations
60 List<RuleFailureModel> pastViolations = pastViolationsLoader.getPastViolations(resource);
61 // Load current source and calculate checksums
62 checksums = getChecksums(pastViolationsLoader.getSource(resource));
64 compareWithPastViolations(context, pastViolations);
69 private void compareWithPastViolations(DecoratorContext context, List<RuleFailureModel> pastViolations) {
70 Multimap<Rule, RuleFailureModel> pastViolationsByRule = LinkedHashMultimap.create();
71 for (RuleFailureModel pastViolation : pastViolations) {
72 Rule rule = ruleFinder.findById(pastViolation.getRuleId());
73 pastViolationsByRule.put(rule, pastViolation);
75 // for each violation, search equivalent past violation
76 for (Violation violation : context.getViolations()) {
77 RuleFailureModel pastViolation = selectPastViolation(violation, pastViolationsByRule);
78 if (pastViolation != null) {
79 // remove violation from past, since would be updated and shouldn't affect other violations anymore
80 pastViolationsByRule.remove(violation.getRule(), pastViolation);
82 String checksum = getChecksumForLine(checksums, violation.getLineId());
83 violationPersister.saveOrUpdateViolation(context.getProject(), violation, pastViolation, checksum);
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 result.add(getChecksum(line));
101 } catch (IOException e) {
102 throw new SonarException("Unable to calculate checksums", e);
107 static String getChecksum(String line) {
108 String reducedLine = StringUtils.replaceChars(line, SPACE_CHARS, "");
109 return DigestUtils.md5Hex(reducedLine);
113 * @return checksum or null if checksum not exists for line
115 private String getChecksumForLine(List<String> checksums, Integer line) {
116 if (line == null || line < 1 || line > checksums.size()) {
119 return checksums.get(line - 1);
123 * Search for past violation.
125 RuleFailureModel selectPastViolation(Violation violation, Multimap<Rule, RuleFailureModel> pastViolationsByRule) {
126 // skip violation, if there is no past violations with same rule
127 if (!pastViolationsByRule.containsKey(violation.getRule())) {
130 Collection<RuleFailureModel> pastViolations = pastViolationsByRule.get(violation.getRule());
131 RuleFailureModel found = selectPastViolationUsingLine(violation, pastViolations);
133 found = selectPastViolationUsingChecksum(violation, pastViolations);
139 * Search for past violation with same message and line.
141 private RuleFailureModel selectPastViolationUsingLine(Violation violation, Collection<RuleFailureModel> pastViolations) {
142 for (RuleFailureModel pastViolation : pastViolations) {
143 if (ObjectUtils.equals(violation.getLineId(), pastViolation.getLine()) && StringUtils.equals(violation.getMessage(), pastViolation.getMessage())) {
144 return pastViolation;
151 * Search for past violation with same message and checksum.
153 private RuleFailureModel selectPastViolationUsingChecksum(Violation violation, Collection<RuleFailureModel> pastViolations) {
154 String checksum = getChecksumForLine(checksums, violation.getLineId());
155 // skip violation, which not attached to line
156 if (checksum == null) {
159 for (RuleFailureModel pastViolation : pastViolations) {
160 String pastChecksum = pastViolation.getChecksum();
161 if (StringUtils.equals(checksum, pastChecksum) && StringUtils.equals(violation.getMessage(), pastViolation.getMessage())) {
162 return pastViolation;
169 public String toString() {
170 return getClass().getSimpleName();