001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2008-2011 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.collect.LinkedHashMultimap;
023    import com.google.common.collect.Lists;
024    import com.google.common.collect.Maps;
025    import com.google.common.collect.Multimap;
026    import org.apache.commons.lang.ObjectUtils;
027    import org.apache.commons.lang.StringUtils;
028    import org.sonar.api.batch.*;
029    import org.sonar.api.database.model.RuleFailureModel;
030    import org.sonar.api.resources.Project;
031    import org.sonar.api.resources.Resource;
032    import org.sonar.api.rules.Violation;
033    import org.sonar.api.violations.ViolationQuery;
034    
035    import java.util.Collection;
036    import java.util.List;
037    import java.util.Map;
038    
039    @DependsUpon({DecoratorBarriers.END_OF_VIOLATIONS_GENERATION, DecoratorBarriers.START_VIOLATION_TRACKING})
040    @DependedUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
041    public class ViolationTrackingDecorator implements Decorator {
042      private ReferenceAnalysis referenceAnalysis;
043      private Map<Violation, RuleFailureModel> referenceViolationsMap = Maps.newIdentityHashMap();
044      private SonarIndex index;
045      private Project project;
046    
047      public ViolationTrackingDecorator(Project project, ReferenceAnalysis referenceAnalysis, SonarIndex index) {
048        this.referenceAnalysis = referenceAnalysis;
049        this.index = index;
050        this.project = project;
051      }
052    
053      public boolean shouldExecuteOnProject(Project project) {
054        return true;
055      }
056    
057      public void decorate(Resource resource, DecoratorContext context) {
058        referenceViolationsMap.clear();
059    
060        ViolationQuery violationQuery = ViolationQuery.create().forResource(resource).setSwitchMode(ViolationQuery.SwitchMode.BOTH);
061        if (!context.getViolations(violationQuery).isEmpty()) {
062          // Load new violations
063          List<Violation> newViolations = prepareNewViolations(context);
064    
065          // Load reference violations
066          List<RuleFailureModel> referenceViolations = referenceAnalysis.getViolations(resource);
067    
068          // Map new violations with old ones
069          mapViolations(newViolations, referenceViolations);
070        }
071      }
072    
073      private List<Violation> prepareNewViolations(DecoratorContext context) {
074        List<Violation> result = Lists.newArrayList();
075        List<String> checksums = SourceChecksum.lineChecksumsOfFile(index.getSource(context.getResource()));
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      Map<Violation, RuleFailureModel> mapViolations(List<Violation> newViolations, List<RuleFailureModel> pastViolations) {
088        Multimap<Integer, RuleFailureModel> pastViolationsByRule = LinkedHashMultimap.create();
089        for (RuleFailureModel pastViolation : pastViolations) {
090          pastViolationsByRule.put(pastViolation.getRuleId(), pastViolation);
091        }
092    
093        // Match the permanent id of the violation. This id is for example set explicitly when injecting manual violations
094        for (Violation newViolation : newViolations) {
095          mapViolation(newViolation,
096              findPastViolationWithSamePermanentId(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
097              pastViolationsByRule, referenceViolationsMap);
098        }
099    
100    
101        // Try first to match violations on same rule with same line and with same checkum (but not necessarily with same message)
102        for (Violation newViolation : newViolations) {
103          if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
104            mapViolation(newViolation,
105                findPastViolationWithSameLineAndChecksum(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
106                pastViolationsByRule, referenceViolationsMap);
107          }
108        }
109    
110        // If each new violation matches an old one we can stop the matching mechanism
111        if (referenceViolationsMap.size() != newViolations.size()) {
112          // Try then to match violations on same rule with same message and with same checkum
113          for (Violation newViolation : newViolations) {
114            if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
115              mapViolation(newViolation,
116                  findPastViolationWithSameChecksumAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
117                  pastViolationsByRule, referenceViolationsMap);
118            }
119          }
120    
121          // Try then to match violations on same rule with same line and with same message
122          for (Violation newViolation : newViolations) {
123            if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
124              mapViolation(newViolation,
125                  findPastViolationWithSameLineAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
126                  pastViolationsByRule, referenceViolationsMap);
127            }
128          }
129    
130          // Last check: match violation if same rule and same checksum but different line and different message
131          // See https://jira.codehaus.org/browse/SONAR-2812
132          for (Violation newViolation : newViolations) {
133            if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
134              mapViolation(newViolation,
135                  findPastViolationWithSameChecksum(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
136                  pastViolationsByRule, referenceViolationsMap);
137            }
138          }
139        }
140        return referenceViolationsMap;
141      }
142    
143      private boolean isNotAlreadyMapped(Violation newViolation, Map<Violation, RuleFailureModel> violationMap) {
144        return !violationMap.containsKey(newViolation);
145      }
146    
147      private RuleFailureModel findPastViolationWithSameChecksum(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
148        for (RuleFailureModel pastViolation : pastViolations) {
149          if (isSameChecksum(newViolation, pastViolation)) {
150            return pastViolation;
151          }
152        }
153        return null;
154      }
155    
156      private RuleFailureModel findPastViolationWithSameLineAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
157        for (RuleFailureModel pastViolation : pastViolations) {
158          if (isSameLine(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
159            return pastViolation;
160          }
161        }
162        return null;
163      }
164    
165      private RuleFailureModel findPastViolationWithSameChecksumAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
166        for (RuleFailureModel pastViolation : pastViolations) {
167          if (isSameChecksum(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
168            return pastViolation;
169          }
170        }
171        return null;
172      }
173    
174      private RuleFailureModel findPastViolationWithSameLineAndChecksum(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
175        for (RuleFailureModel pastViolation : pastViolations) {
176          if (isSameLine(newViolation, pastViolation) && isSameChecksum(newViolation, pastViolation)) {
177            return pastViolation;
178          }
179        }
180        return null;
181      }
182    
183      private RuleFailureModel findPastViolationWithSamePermanentId(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
184        for (RuleFailureModel pastViolation : pastViolations) {
185          if (isSamePermanentId(newViolation, pastViolation)) {
186            return pastViolation;
187          }
188        }
189        return null;
190      }
191    
192      private boolean isSameChecksum(Violation newViolation, RuleFailureModel pastViolation) {
193        return StringUtils.equals(pastViolation.getChecksum(), newViolation.getChecksum());
194      }
195    
196      private boolean isSameLine(Violation newViolation, RuleFailureModel pastViolation) {
197        return ObjectUtils.equals(pastViolation.getLine(), newViolation.getLineId());
198      }
199    
200      private boolean isSameMessage(Violation newViolation, RuleFailureModel pastViolation) {
201        return StringUtils.equals(RuleFailureModel.abbreviateMessage(newViolation.getMessage()), pastViolation.getMessage());
202      }
203    
204      private boolean isSamePermanentId(Violation newViolation, RuleFailureModel pastViolation) {
205        return newViolation.getPermanentId() != null && newViolation.getPermanentId().equals(pastViolation.getPermanentId());
206      }
207    
208      private void mapViolation(Violation newViolation, RuleFailureModel pastViolation,
209                                Multimap<Integer, RuleFailureModel> pastViolationsByRule, Map<Violation, RuleFailureModel> violationMap) {
210        if (pastViolation != null) {
211          newViolation.setCreatedAt(pastViolation.getCreatedAt());
212          newViolation.setPermanentId(pastViolation.getPermanentId());
213          newViolation.setSwitchedOff(pastViolation.isSwitchedOff());
214          newViolation.setCommitter(pastViolation.getCommitter());
215          newViolation.setNew(false);
216          pastViolationsByRule.remove(newViolation.getRule().getId(), pastViolation);
217          violationMap.put(newViolation, pastViolation);
218    
219        } else {
220          newViolation.setNew(true);
221          newViolation.setCreatedAt(project.getAnalysisDate());
222        }
223      }
224    
225      @Override
226      public String toString() {
227        return getClass().getSimpleName();
228      }
229    
230    }