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      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        // Try first to match violations on same rule with same line and with same checkum (but not necessarily with same message)
094        for (Violation newViolation : newViolations) {
095          mapViolation(newViolation,
096              findPastViolationWithSameLineAndChecksum(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
097              pastViolationsByRule, referenceViolationsMap);
098        }
099    
100        // If each new violation matches an old one we can stop the matching mechanism
101        if (referenceViolationsMap.size() != newViolations.size()) {
102          // Try then to match violations on same rule with same message and with same checkum
103          for (Violation newViolation : newViolations) {
104            if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
105              mapViolation(newViolation,
106                  findPastViolationWithSameChecksumAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
107                  pastViolationsByRule, referenceViolationsMap);
108            }
109          }
110    
111          // Try then to match violations on same rule with same line and with same message
112          for (Violation newViolation : newViolations) {
113            if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
114              mapViolation(newViolation,
115                  findPastViolationWithSameLineAndMessage(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
116                  pastViolationsByRule, referenceViolationsMap);
117            }
118          }
119    
120          // Last check: match violation if same rule and same checksum but different line and different message
121          // See https://jira.codehaus.org/browse/SONAR-2812
122          for (Violation newViolation : newViolations) {
123            if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
124              mapViolation(newViolation,
125                  findPastViolationWithSameChecksum(newViolation, pastViolationsByRule.get(newViolation.getRule().getId())),
126                  pastViolationsByRule, referenceViolationsMap);
127            }
128          }
129        }
130        return referenceViolationsMap;
131      }
132    
133      private boolean isNotAlreadyMapped(Violation newViolation, Map<Violation, RuleFailureModel> violationMap) {
134        return !violationMap.containsKey(newViolation);
135      }
136    
137      private RuleFailureModel findPastViolationWithSameChecksum(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
138        for (RuleFailureModel pastViolation : pastViolations) {
139          if (isSameChecksum(newViolation, pastViolation)) {
140            return pastViolation;
141          }
142        }
143        return null;
144      }
145    
146      private RuleFailureModel findPastViolationWithSameLineAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
147        for (RuleFailureModel pastViolation : pastViolations) {
148          if (isSameLine(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
149            return pastViolation;
150          }
151        }
152        return null;
153      }
154    
155      private RuleFailureModel findPastViolationWithSameChecksumAndMessage(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
156        for (RuleFailureModel pastViolation : pastViolations) {
157          if (isSameChecksum(newViolation, pastViolation) && isSameMessage(newViolation, pastViolation)) {
158            return pastViolation;
159          }
160        }
161        return null;
162      }
163    
164      private RuleFailureModel findPastViolationWithSameLineAndChecksum(Violation newViolation, Collection<RuleFailureModel> pastViolations) {
165        for (RuleFailureModel pastViolation : pastViolations) {
166          if (isSameLine(newViolation, pastViolation) && isSameChecksum(newViolation, pastViolation)) {
167            return pastViolation;
168          }
169        }
170        return null;
171      }
172    
173      private boolean isSameChecksum(Violation newViolation, RuleFailureModel pastViolation) {
174        return StringUtils.equals(pastViolation.getChecksum(), newViolation.getChecksum());
175      }
176    
177      private boolean isSameLine(Violation newViolation, RuleFailureModel pastViolation) {
178        return ObjectUtils.equals(pastViolation.getLine(), newViolation.getLineId());
179      }
180    
181      private boolean isSameMessage(Violation newViolation, RuleFailureModel pastViolation) {
182        return StringUtils.equals(RuleFailureModel.abbreviateMessage(newViolation.getMessage()), pastViolation.getMessage());
183      }
184    
185      private void mapViolation(Violation newViolation, RuleFailureModel pastViolation,
186                                Multimap<Integer, RuleFailureModel> pastViolationsByRule, Map<Violation, RuleFailureModel> violationMap) {
187        if (pastViolation != null) {
188          newViolation.setCreatedAt(pastViolation.getCreatedAt());
189          newViolation.setSwitchedOff(pastViolation.isSwitchedOff());
190          newViolation.setNew(false);
191          pastViolationsByRule.remove(newViolation.getRule().getId(), pastViolation);
192          violationMap.put(newViolation, pastViolation);
193          
194        } else {
195          newViolation.setNew(true);
196          newViolation.setCreatedAt(project.getAnalysisDate());
197        }
198      }
199    
200      @Override
201      public String toString() {
202        return getClass().getSimpleName();
203      }
204    
205    }