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     */
020    package org.sonar.plugins.core.timemachine;
021    
022    import com.google.common.annotations.VisibleForTesting;
023    import com.google.common.collect.*;
024    import org.apache.commons.lang.ObjectUtils;
025    import org.apache.commons.lang.StringUtils;
026    import org.sonar.api.batch.*;
027    import org.sonar.api.database.model.RuleFailureModel;
028    import org.sonar.api.resources.Project;
029    import org.sonar.api.resources.Resource;
030    import org.sonar.api.rules.Violation;
031    import org.sonar.api.violations.ViolationQuery;
032    
033    import java.util.*;
034    
035    @DependsUpon({DecoratorBarriers.END_OF_VIOLATIONS_GENERATION, DecoratorBarriers.START_VIOLATION_TRACKING})
036    @DependedUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
037    public 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) {
128                  for (RuleFailureModel pastViolation : pastViolationsByRule.get(newViolation.getRule().getId())) {
129                    if (pastViolation.getLine() != null) {
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    }