2 * Sonar, open source software quality management tool.
3 * Copyright (C) 2008-2011 SonarSource
4 * mailto:contact AT sonarsource DOT com
6 * Sonar 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 * Sonar 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
17 * License along with Sonar; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
20 package org.sonar.plugins.core.timemachine;
22 import java.io.IOException;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.IdentityHashMap;
26 import java.util.List;
29 import org.apache.commons.codec.digest.DigestUtils;
30 import org.apache.commons.io.IOUtils;
31 import org.apache.commons.lang.StringUtils;
32 import org.codehaus.plexus.util.StringInputStream;
33 import org.sonar.api.batch.Decorator;
34 import org.sonar.api.batch.DecoratorBarriers;
35 import org.sonar.api.batch.DecoratorContext;
36 import org.sonar.api.batch.DependedUpon;
37 import org.sonar.api.batch.DependsUpon;
38 import org.sonar.api.database.model.RuleFailureModel;
39 import org.sonar.api.database.model.SnapshotSource;
40 import org.sonar.api.resources.Project;
41 import org.sonar.api.resources.Resource;
42 import org.sonar.api.rules.Violation;
43 import org.sonar.api.utils.SonarException;
44 import org.sonar.batch.components.PastViolationsLoader;
45 import org.sonar.batch.index.ViolationPersister;
47 import com.google.common.collect.LinkedHashMultimap;
48 import com.google.common.collect.Lists;
49 import com.google.common.collect.Multimap;
51 @DependsUpon(DecoratorBarriers.END_OF_VIOLATIONS_GENERATION)
52 @DependedUpon("ViolationPersisterDecorator")
53 /* temporary workaround - see NewViolationsDecorator */
54 public class ViolationPersisterDecorator implements Decorator {
57 * Those chars would be ignored during generation of checksums.
59 private static final String SPACE_CHARS = "\t\n\r ";
61 private PastViolationsLoader pastViolationsLoader;
62 private ViolationPersister violationPersister;
64 List<String> checksums;
66 public ViolationPersisterDecorator(PastViolationsLoader pastViolationsLoader, ViolationPersister violationPersister) {
67 this.pastViolationsLoader = pastViolationsLoader;
68 this.violationPersister = violationPersister;
71 public boolean shouldExecuteOnProject(Project project) {
75 public void decorate(Resource resource, DecoratorContext context) {
76 if (context.getViolations().isEmpty()) {
79 // Load new violations
80 List<Violation> newViolations = context.getViolations();
82 // Load past violations
83 List<RuleFailureModel> pastViolations = pastViolationsLoader.getPastViolations(resource);
85 // Load current source code and calculate checksums for each line
86 checksums = getChecksums(pastViolationsLoader.getSource(resource));
88 // Map new violations with old ones
89 Map<Violation, RuleFailureModel> violationMap = mapViolations(newViolations, pastViolations);
91 for (Violation newViolation : newViolations) {
92 String checksum = getChecksumForLine(checksums, newViolation.getLineId());
93 violationPersister.saveViolation(context.getProject(), newViolation, violationMap.get(newViolation), checksum);
95 violationPersister.commit();
100 Map<Violation, RuleFailureModel> mapViolations(List<Violation> newViolations, List<RuleFailureModel> pastViolations) {
101 Map<Violation, RuleFailureModel> violationMap = new IdentityHashMap<Violation, RuleFailureModel>();
103 Multimap<Integer, RuleFailureModel> pastViolationsByRule = LinkedHashMultimap.create();
104 for (RuleFailureModel pastViolation : pastViolations) {
105 pastViolationsByRule.put(pastViolation.getRuleId(), pastViolation);
108 // Try first an exact matching : same rule, same message, same line and same checkum
109 for (Violation newViolation : newViolations) {
110 mapViolation(newViolation,
111 findPastViolationWithSameLineAndChecksumAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
112 pastViolationsByRule, violationMap);
115 // If each new violation matches an old one we can stop the matching mechanism
116 if (violationMap.size() != newViolations.size()) {
118 // Try then to match violations on same rule with same message and with same checkum
119 for (Violation newViolation : newViolations) {
120 if (isNotAlreadyMapped(newViolation, violationMap)) {
121 mapViolation(newViolation,
122 findPastViolationWithSameChecksumAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
123 pastViolationsByRule, violationMap);
127 // Try then to match violations on same rule with same line and with same message
128 for (Violation newViolation : newViolations) {
129 if (isNotAlreadyMapped(newViolation, violationMap)) {
130 mapViolation(newViolation,
131 findPastViolationWithSameLineAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
132 pastViolationsByRule, violationMap);
140 private final boolean isNotAlreadyMapped(Violation newViolation, Map<Violation, RuleFailureModel> violationMap) {
141 return violationMap.get(newViolation) == null;
144 private RuleFailureModel findPastViolationWithSameLineAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
145 for (RuleFailureModel pastViolation : pastViolations) {
146 if (isSameLine(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
147 return pastViolation;
153 private RuleFailureModel findPastViolationWithSameChecksumAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
154 for (RuleFailureModel pastViolation : pastViolations) {
155 if (isSameChecksum(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
156 return pastViolation;
162 private RuleFailureModel findPastViolationWithSameLineAndChecksumAndMessage(Violation newViolation,
163 Collection<RuleFailureModel> pastViolations) {
164 for (RuleFailureModel pastViolation : pastViolations) {
165 if (isSameLine(newViolation, pastViolation) && isSameChecksum(newViolation, pastViolation)
166 && isSameMessage(newViolation, pastViolation)) {
167 return pastViolation;
173 private final boolean isSameChecksum(Violation newViolation, RuleFailureModel pastViolation) {
174 return pastViolation.getChecksum().equals(getChecksumForLine(checksums, newViolation.getLineId()));
177 private final boolean isSameLine(Violation newViolation, RuleFailureModel pastViolation) {
178 return pastViolation.getLine() == newViolation.getLineId();
181 private final boolean isSameMessage(Violation newViolation, RuleFailureModel pastViolation) {
182 return StringUtils.equals(RuleFailureModel.abbreviateMessage(newViolation.getMessage()), pastViolation.getMessage());
185 private void mapViolation(Violation newViolation, RuleFailureModel pastViolation,
186 Multimap<Integer, RuleFailureModel> pastViolationsByRule, Map<Violation, RuleFailureModel> violationMap) {
187 if (pastViolation != null) {
188 pastViolationsByRule.remove(newViolation.getRule().getId(), pastViolation);
189 violationMap.put(newViolation, pastViolation);
194 * @return checksums, never null
196 private List<String> getChecksums(SnapshotSource source) {
197 return source == null || source.getData() == null ? Collections.<String> emptyList() : getChecksums(source.getData());
200 static List<String> getChecksums(String data) {
201 List<String> result = Lists.newArrayList();
202 StringInputStream stream = new StringInputStream(data);
204 List<String> lines = IOUtils.readLines(stream);
205 for (String line : lines) {
206 result.add(getChecksum(line));
208 } catch (IOException e) {
209 throw new SonarException("Unable to calculate checksums", e);
212 IOUtils.closeQuietly(stream);
217 static String getChecksum(String line) {
218 String reducedLine = StringUtils.replaceChars(line, SPACE_CHARS, "");
219 return DigestUtils.md5Hex(reducedLine);
223 * @return checksum or null if checksum not exists for line
225 private String getChecksumForLine(List<String> checksums, Integer line) {
226 if (line == null || line < 1 || line > checksums.size()) {
229 return checksums.get(line - 1);
233 public String toString() {
234 return getClass().getSimpleName();