001/*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2012 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * Sonar is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * Sonar is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public
017 * License along with Sonar; if not, write to the Free Software
018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019 */
020package org.sonar.plugins.core.timemachine;
021
022import com.google.common.annotations.VisibleForTesting;
023import com.google.common.collect.*;
024import org.apache.commons.lang.ObjectUtils;
025import org.apache.commons.lang.StringUtils;
026import org.sonar.api.batch.*;
027import org.sonar.api.database.model.RuleFailureModel;
028import org.sonar.api.resources.Project;
029import org.sonar.api.resources.Resource;
030import org.sonar.api.rules.Violation;
031import org.sonar.api.violations.ViolationQuery;
032
033import java.util.*;
034
035@DependsUpon({DecoratorBarriers.END_OF_VIOLATIONS_GENERATION, DecoratorBarriers.START_VIOLATION_TRACKING})
036@DependedUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
037public class ViolationTrackingDecorator implements Decorator {
038  private ReferenceAnalysis referenceAnalysis;
039  private Map<Violation, RuleFailureModel> referenceViolationsMap = Maps.newIdentityHashMap();
040  private SonarIndex index;
041  private Project project;
042
043  public ViolationTrackingDecorator(Project project, ReferenceAnalysis referenceAnalysis, SonarIndex index) {
044    this.referenceAnalysis = referenceAnalysis;
045    this.index = index;
046    this.project = project;
047  }
048
049  public boolean shouldExecuteOnProject(Project project) {
050    return true;
051  }
052
053  public void decorate(Resource resource, DecoratorContext context) {
054    referenceViolationsMap.clear();
055
056    ViolationQuery violationQuery = ViolationQuery.create().forResource(resource).setSwitchMode(ViolationQuery.SwitchMode.BOTH);
057    if (context.getViolations(violationQuery).isEmpty()) {
058      return;
059    }
060
061    String source = index.getSource(resource);
062
063    // Load new violations
064    List<Violation> newViolations = prepareNewViolations(context, source);
065
066    // Load reference violations
067    List<RuleFailureModel> referenceViolations = referenceAnalysis.getViolations(resource);
068
069    // Map new violations with old ones
070    mapViolations(newViolations, referenceViolations, source, resource);
071  }
072
073  private List<Violation> prepareNewViolations(DecoratorContext context, String source) {
074    List<Violation> result = Lists.newArrayList();
075    List<String> checksums = SourceChecksum.lineChecksumsOfFile(source);
076    for (Violation violation : context.getViolations()) {
077      violation.setChecksum(SourceChecksum.getChecksumForLine(checksums, violation.getLineId()));
078      result.add(violation);
079    }
080    return result;
081  }
082
083  public RuleFailureModel getReferenceViolation(Violation violation) {
084    return referenceViolationsMap.get(violation);
085  }
086
087  @VisibleForTesting
088  Map<Violation, RuleFailureModel> mapViolations(List<Violation> newViolations, List<RuleFailureModel> pastViolations) {
089    return mapViolations(newViolations, pastViolations, null, null);
090  }
091
092  @VisibleForTesting
093  Map<Violation, RuleFailureModel> mapViolations(List<Violation> newViolations, List<RuleFailureModel> pastViolations, String source, Resource resource) {
094    Multimap<Integer, RuleFailureModel> pastViolationsByRule = LinkedHashMultimap.create();
095    for (RuleFailureModel pastViolation : pastViolations) {
096      pastViolationsByRule.put(pastViolation.getRuleId(), pastViolation);
097    }
098
099    // Match the permanent id of the violation. This id is for example set explicitly when injecting manual violations
100    for (Violation newViolation : newViolations) {
101      mapViolation(newViolation,
102          findPastViolationWithSamePermanentId(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
103          pastViolationsByRule, referenceViolationsMap);
104    }
105
106    // Try first to match violations on same rule with same line and with same checksum (but not necessarily with same message)
107    for (Violation newViolation : newViolations) {
108      if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
109        mapViolation(newViolation,
110            findPastViolationWithSameLineAndChecksum(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
111            pastViolationsByRule, referenceViolationsMap);
112      }
113    }
114
115    // If each new violation matches an old one we can stop the matching mechanism
116    if (referenceViolationsMap.size() != newViolations.size()) {
117
118      // SONAR-3072
119      ViolationTrackingBlocksRecognizer rec = null;
120      if (source != null && resource != null) {
121        String referenceSource = referenceAnalysis.getSource(resource);
122        if (referenceSource != null) {
123          rec = new ViolationTrackingBlocksRecognizer(referenceSource, source);
124
125          List<ViolationPair> possiblePairs = Lists.newArrayList();
126          for (Violation newViolation : newViolations) {
127            if (newViolation.getLineId() != null && rec.isValidLineInSource(newViolation.getLineId() - 1)) {
128              for (RuleFailureModel pastViolation : pastViolationsByRule.get(newViolation.getRule().getId())) {
129                if (pastViolation.getLine() != null && rec.isValidLineInReference(pastViolation.getLine() - 1)) {
130                  int weight = rec.computeLengthOfMaximalBlock(pastViolation.getLine() - 1, newViolation.getLineId() - 1);
131                  possiblePairs.add(new ViolationPair(pastViolation, newViolation, weight));
132                }
133              }
134            }
135          }
136          Collections.sort(possiblePairs, ViolationPair.COMPARATOR);
137
138          Set<RuleFailureModel> pp = Sets.newHashSet(pastViolations);
139          for (ViolationPair pair : possiblePairs) {
140            Violation newViolation = pair.getNewViolation();
141            RuleFailureModel pastViolation = pair.getPastViolation();
142            if (isNotAlreadyMapped(newViolation, referenceViolationsMap) && pp.contains(pastViolation)) {
143              pp.remove(pastViolation);
144              mapViolation(newViolation, pastViolation, pastViolationsByRule, referenceViolationsMap);
145            }
146          }
147        }
148      }
149
150      // Try then to match violations on same rule with same message and with same checksum
151      for (Violation newViolation : newViolations) {
152        if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
153          mapViolation(newViolation,
154              findPastViolationWithSameChecksumAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
155              pastViolationsByRule, referenceViolationsMap);
156        }
157      }
158
159      // Try then to match violations on same rule with same line and with same message
160      for (Violation newViolation : newViolations) {
161        if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
162          mapViolation(newViolation,
163              findPastViolationWithSameLineAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
164              pastViolationsByRule, referenceViolationsMap);
165        }
166      }
167
168      // Last check: match violation if same rule and same checksum but different line and different message
169      // See SONAR-2812
170      for (Violation newViolation : newViolations) {
171        if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
172          mapViolation(newViolation,
173              findPastViolationWithSameChecksum(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
174              pastViolationsByRule, referenceViolationsMap);
175        }
176      }
177    }
178    return referenceViolationsMap;
179  }
180
181  private boolean isNotAlreadyMapped(Violation newViolation, Map<Violation, RuleFailureModel> violationMap) {
182    return !violationMap.containsKey(newViolation);
183  }
184
185  private RuleFailureModel findPastViolationWithSameChecksum(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
186    for (RuleFailureModel pastViolation : pastViolations) {
187      if (isSameChecksum(newViolation, pastViolation)) {
188        return pastViolation;
189      }
190    }
191    return null;
192  }
193
194  private RuleFailureModel findPastViolationWithSameLineAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
195    for (RuleFailureModel pastViolation : pastViolations) {
196      if (isSameLine(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
197        return pastViolation;
198      }
199    }
200    return null;
201  }
202
203  private RuleFailureModel findPastViolationWithSameChecksumAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
204    for (RuleFailureModel pastViolation : pastViolations) {
205      if (isSameChecksum(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
206        return pastViolation;
207      }
208    }
209    return null;
210  }
211
212  private RuleFailureModel findPastViolationWithSameLineAndChecksum(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
213    for (RuleFailureModel pastViolation : pastViolations) {
214      if (isSameLine(newViolation, pastViolation) && isSameChecksum(newViolation, pastViolation)) {
215        return pastViolation;
216      }
217    }
218    return null;
219  }
220
221  private RuleFailureModel findPastViolationWithSamePermanentId(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
222    for (RuleFailureModel pastViolation : pastViolations) {
223      if (isSamePermanentId(newViolation, pastViolation)) {
224        return pastViolation;
225      }
226    }
227    return null;
228  }
229
230  private boolean isSameChecksum(Violation newViolation, RuleFailureModel pastViolation) {
231    return StringUtils.equals(pastViolation.getChecksum(), newViolation.getChecksum());
232  }
233
234  private boolean isSameLine(Violation newViolation, RuleFailureModel pastViolation) {
235    return ObjectUtils.equals(pastViolation.getLine(), newViolation.getLineId());
236  }
237
238  private boolean isSameMessage(Violation newViolation, RuleFailureModel pastViolation) {
239    return StringUtils.equals(RuleFailureModel.abbreviateMessage(newViolation.getMessage()), pastViolation.getMessage());
240  }
241
242  private boolean isSamePermanentId(Violation newViolation, RuleFailureModel pastViolation) {
243    return newViolation.getPermanentId() != null && newViolation.getPermanentId().equals(pastViolation.getPermanentId());
244  }
245
246  private void mapViolation(Violation newViolation, RuleFailureModel pastViolation,
247      Multimap<Integer, RuleFailureModel> pastViolationsByRule, Map<Violation, RuleFailureModel> violationMap) {
248    if (pastViolation != null) {
249      newViolation.setCreatedAt(pastViolation.getCreatedAt());
250      newViolation.setPermanentId(pastViolation.getPermanentId());
251      newViolation.setSwitchedOff(pastViolation.isSwitchedOff());
252      newViolation.setPersonId(pastViolation.getPersonId());
253      newViolation.setNew(false);
254      pastViolationsByRule.remove(newViolation.getRule().getId(), pastViolation);
255      violationMap.put(newViolation, pastViolation);
256
257    } else {
258      newViolation.setNew(true);
259      newViolation.setCreatedAt(project.getAnalysisDate());
260    }
261  }
262
263  @Override
264  public String toString() {
265    return getClass().getSimpleName();
266  }
267
268}