]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3755 Added issues decorators
authorJulien Lancelot <julien.lancelot@gmail.com>
Mon, 15 Apr 2013 12:32:19 +0000 (14:32 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Mon, 15 Apr 2013 12:32:19 +0000 (14:32 +0200)
15 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/WeightedViolationsDecorator.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java
sonar-batch/src/main/java/org/sonar/batch/issue/IssuesDecorator.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/issue/IssuesDensityDecorator.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/issue/SeverityUtils.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/issue/WeightedIssuesDecorator.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/issue/IssuesDecoratorTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/issue/IssuesDensityDecoratorTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/issue/WeightedIssuesDecoratorTest.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-ws-client/src/main/java/org/sonar/wsclient/services/Issue.java
sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/IssueUnmarshaller.java
sonar-ws-client/src/test/resources/issues/single_issue.json

index a6d23cc7dc20ea859c3b158f73b90a7f6374ed7c..2047c8a28534f35bffd20d4a4a147c8eedbaf620 100644 (file)
@@ -25,6 +25,9 @@ import org.sonar.api.*;
 import org.sonar.api.checks.NoSonarFilter;
 import org.sonar.api.notifications.NotificationDispatcherMetadata;
 import org.sonar.api.resources.Java;
+import org.sonar.batch.issue.IssuesDecorator;
+import org.sonar.batch.issue.IssuesDensityDecorator;
+import org.sonar.batch.issue.WeightedIssuesDecorator;
 import org.sonar.core.timemachine.Periods;
 import org.sonar.plugins.core.batch.IndexProjectPostJob;
 import org.sonar.plugins.core.charts.DistributionAreaChart;
@@ -394,8 +397,11 @@ public final class CorePlugin extends SonarPlugin {
         CheckAlertThresholds.class,
         GenerateAlertEvents.class,
         ViolationsDecorator.class,
+        IssuesDecorator.class,
         WeightedViolationsDecorator.class,
+        WeightedIssuesDecorator.class,
         ViolationsDensityDecorator.class,
+        IssuesDensityDecorator.class,
         LineCoverageDecorator.class,
         CoverageDecorator.class,
         BranchCoverageDecorator.class,
index b8b7988b8169abe27176b3dc01ee1af3cdcb6e51..301aa72dc88642db86df931a96dd069fc52b485a 100644 (file)
@@ -140,98 +140,10 @@ public class IssueTrackingDecorator implements Decorator {
       if (source != null && resource != null && hasLastScan) {
         String referenceSource = lastSnapshots.getSource(resource);
         if (referenceSource != null) {
-          HashedSequence<StringText> hashedReference = HashedSequence.wrap(new StringText(referenceSource), StringTextComparator.IGNORE_WHITESPACE);
-          HashedSequence<StringText> hashedSource = HashedSequence.wrap(new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
-          HashedSequenceComparator<StringText> hashedComparator = new HashedSequenceComparator<StringText>(StringTextComparator.IGNORE_WHITESPACE);
-
-          ViolationTrackingBlocksRecognizer rec = new ViolationTrackingBlocksRecognizer(hashedReference, hashedSource, hashedComparator);
-
-          Multimap<Integer, DefaultIssue> newIssuesByLines = newIssuesByLines(newIssues, rec);
-          Multimap<Integer, IssueDto> lastIssuesByLines = lastIssuesByLines(unmappedLastIssues, rec);
-
-          RollingHashSequence<HashedSequence<StringText>> a = RollingHashSequence.wrap(hashedReference, hashedComparator, 5);
-          RollingHashSequence<HashedSequence<StringText>> b = RollingHashSequence.wrap(hashedSource, hashedComparator, 5);
-          RollingHashSequenceComparator<HashedSequence<StringText>> cmp = new RollingHashSequenceComparator<HashedSequence<StringText>>(hashedComparator);
-
-          Map<Integer, HashOccurrence> map = Maps.newHashMap();
-
-          for (Integer line : lastIssuesByLines.keySet()) {
-            int hash = cmp.hash(a, line - 1);
-            HashOccurrence hashOccurrence = map.get(hash);
-            if (hashOccurrence == null) {
-              // first occurrence in A
-              hashOccurrence = new HashOccurrence();
-              hashOccurrence.lineA = line;
-              hashOccurrence.countA = 1;
-              map.put(hash, hashOccurrence);
-            } else {
-              hashOccurrence.countA++;
-            }
-          }
-
-          for (Integer line : newIssuesByLines.keySet()) {
-            int hash = cmp.hash(b, line - 1);
-            HashOccurrence hashOccurrence = map.get(hash);
-            if (hashOccurrence != null) {
-              hashOccurrence.lineB = line;
-              hashOccurrence.countB++;
-            }
-          }
-
-          for (HashOccurrence hashOccurrence : map.values()) {
-            if (hashOccurrence.countA == 1 && hashOccurrence.countB == 1) {
-              // Guaranteed that lineA has been moved to lineB, so we can map all issues on lineA to all issues on lineB
-              map(newIssuesByLines.get(hashOccurrence.lineB), lastIssuesByLines.get(hashOccurrence.lineA), lastIssuesByRule);
-              lastIssuesByLines.removeAll(hashOccurrence.lineA);
-              newIssuesByLines.removeAll(hashOccurrence.lineB);
-            }
-          }
-
-          // Check if remaining number of lines exceeds threshold
-          if (lastIssuesByLines.keySet().size() * newIssuesByLines.keySet().size() < 250000) {
-            List<LinePair> possibleLinePairs = Lists.newArrayList();
-            for (Integer oldLine : lastIssuesByLines.keySet()) {
-              for (Integer newLine : newIssuesByLines.keySet()) {
-                int weight = rec.computeLengthOfMaximalBlock(oldLine - 1, newLine - 1);
-                possibleLinePairs.add(new LinePair(oldLine, newLine, weight));
-              }
-            }
-            Collections.sort(possibleLinePairs, LINE_PAIR_COMPARATOR);
-            for (LinePair linePair : possibleLinePairs) {
-              // High probability that lineA has been moved to lineB, so we can map all Issues on lineA to all Issues on lineB
-              map(newIssuesByLines.get(linePair.lineB), lastIssuesByLines.get(linePair.lineA), lastIssuesByRule);
-            }
-          }
-        }
-      }
-
-      // Try then to match issues on same rule with same message and with same checksum
-      for (DefaultIssue newIssue : newIssues) {
-        if (isNotAlreadyMapped(newIssue)) {
-          mapIssue(newIssue,
-              findLastIssueWithSameChecksumAndMessage(newIssue, lastIssuesByRule.get(getRule(newIssue))),
-              lastIssuesByRule, referenceIssuesMap);
-        }
-      }
-
-      // Try then to match issues on same rule with same line and with same message
-      for (DefaultIssue newIssue : newIssues) {
-        if (isNotAlreadyMapped(newIssue)) {
-          mapIssue(newIssue,
-              findLastIssueWithSameLineAndMessage(newIssue, lastIssuesByRule.get(getRule(newIssue))),
-              lastIssuesByRule, referenceIssuesMap);
-        }
-      }
-
-      // Last check: match issue if same rule and same checksum but different line and different message
-      // See SONAR-2812
-      for (DefaultIssue newIssue : newIssues) {
-        if (isNotAlreadyMapped(newIssue)) {
-          mapIssue(newIssue,
-              findLastIssueWithSameChecksum(newIssue, lastIssuesByRule.get(getRule(newIssue))),
-              lastIssuesByRule, referenceIssuesMap);
+          mapNewissues(referenceSource, newIssues, lastIssuesByRule, source);
         }
       }
+      mapIssuesOnSameRule(newIssues, lastIssuesByRule);
     }
 
     unmappedLastIssues.clear();
@@ -262,6 +174,101 @@ public class IssueTrackingDecorator implements Decorator {
     }
   }
 
+  private void mapNewissues(String referenceSource, List<DefaultIssue> newIssues, Multimap<Integer, IssueDto> lastIssuesByRule, String source){
+    HashedSequence<StringText> hashedReference = HashedSequence.wrap(new StringText(referenceSource), StringTextComparator.IGNORE_WHITESPACE);
+    HashedSequence<StringText> hashedSource = HashedSequence.wrap(new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+    HashedSequenceComparator<StringText> hashedComparator = new HashedSequenceComparator<StringText>(StringTextComparator.IGNORE_WHITESPACE);
+
+    ViolationTrackingBlocksRecognizer rec = new ViolationTrackingBlocksRecognizer(hashedReference, hashedSource, hashedComparator);
+
+    Multimap<Integer, DefaultIssue> newIssuesByLines = newIssuesByLines(newIssues, rec);
+    Multimap<Integer, IssueDto> lastIssuesByLines = lastIssuesByLines(unmappedLastIssues, rec);
+
+    RollingHashSequence<HashedSequence<StringText>> a = RollingHashSequence.wrap(hashedReference, hashedComparator, 5);
+    RollingHashSequence<HashedSequence<StringText>> b = RollingHashSequence.wrap(hashedSource, hashedComparator, 5);
+    RollingHashSequenceComparator<HashedSequence<StringText>> cmp = new RollingHashSequenceComparator<HashedSequence<StringText>>(hashedComparator);
+
+    Map<Integer, HashOccurrence> map = Maps.newHashMap();
+
+    for (Integer line : lastIssuesByLines.keySet()) {
+      int hash = cmp.hash(a, line - 1);
+      HashOccurrence hashOccurrence = map.get(hash);
+      if (hashOccurrence == null) {
+        // first occurrence in A
+        hashOccurrence = new HashOccurrence();
+        hashOccurrence.lineA = line;
+        hashOccurrence.countA = 1;
+        map.put(hash, hashOccurrence);
+      } else {
+        hashOccurrence.countA++;
+      }
+    }
+
+    for (Integer line : newIssuesByLines.keySet()) {
+      int hash = cmp.hash(b, line - 1);
+      HashOccurrence hashOccurrence = map.get(hash);
+      if (hashOccurrence != null) {
+        hashOccurrence.lineB = line;
+        hashOccurrence.countB++;
+      }
+    }
+
+    for (HashOccurrence hashOccurrence : map.values()) {
+      if (hashOccurrence.countA == 1 && hashOccurrence.countB == 1) {
+        // Guaranteed that lineA has been moved to lineB, so we can map all issues on lineA to all issues on lineB
+        map(newIssuesByLines.get(hashOccurrence.lineB), lastIssuesByLines.get(hashOccurrence.lineA), lastIssuesByRule);
+        lastIssuesByLines.removeAll(hashOccurrence.lineA);
+        newIssuesByLines.removeAll(hashOccurrence.lineB);
+      }
+    }
+
+    // Check if remaining number of lines exceeds threshold
+    if (lastIssuesByLines.keySet().size() * newIssuesByLines.keySet().size() < 250000) {
+      List<LinePair> possibleLinePairs = Lists.newArrayList();
+      for (Integer oldLine : lastIssuesByLines.keySet()) {
+        for (Integer newLine : newIssuesByLines.keySet()) {
+          int weight = rec.computeLengthOfMaximalBlock(oldLine - 1, newLine - 1);
+          possibleLinePairs.add(new LinePair(oldLine, newLine, weight));
+        }
+      }
+      Collections.sort(possibleLinePairs, LINE_PAIR_COMPARATOR);
+      for (LinePair linePair : possibleLinePairs) {
+        // High probability that lineA has been moved to lineB, so we can map all Issues on lineA to all Issues on lineB
+        map(newIssuesByLines.get(linePair.lineB), lastIssuesByLines.get(linePair.lineA), lastIssuesByRule);
+      }
+    }
+  }
+
+  private void mapIssuesOnSameRule(List<DefaultIssue> newIssues, Multimap<Integer, IssueDto> lastIssuesByRule){
+    // Try then to match issues on same rule with same message and with same checksum
+    for (DefaultIssue newIssue : newIssues) {
+      if (isNotAlreadyMapped(newIssue)) {
+        mapIssue(newIssue,
+            findLastIssueWithSameChecksumAndMessage(newIssue, lastIssuesByRule.get(getRule(newIssue))),
+            lastIssuesByRule, referenceIssuesMap);
+      }
+    }
+
+    // Try then to match issues on same rule with same line and with same message
+    for (DefaultIssue newIssue : newIssues) {
+      if (isNotAlreadyMapped(newIssue)) {
+        mapIssue(newIssue,
+            findLastIssueWithSameLineAndMessage(newIssue, lastIssuesByRule.get(getRule(newIssue))),
+            lastIssuesByRule, referenceIssuesMap);
+      }
+    }
+
+    // Last check: match issue if same rule and same checksum but different line and different message
+    // See SONAR-2812
+    for (DefaultIssue newIssue : newIssues) {
+      if (isNotAlreadyMapped(newIssue)) {
+        mapIssue(newIssue,
+            findLastIssueWithSameChecksum(newIssue, lastIssuesByRule.get(getRule(newIssue))),
+            lastIssuesByRule, referenceIssuesMap);
+      }
+    }
+  }
+
   @VisibleForTesting
   IssueDto getReferenceIssue(DefaultIssue issue) {
     return referenceIssuesMap.get(issue);
@@ -388,12 +395,17 @@ public class IssueTrackingDecorator implements Decorator {
 
   private void mapIssue(DefaultIssue newIssue, IssueDto pastIssue, Multimap<Integer, IssueDto> lastIssuesByRule, Map<DefaultIssue, IssueDto> issueMap) {
     if (pastIssue != null) {
+      newIssue.setKey(pastIssue.getUuid());
+      if (pastIssue.isManualSeverity()) {
+        newIssue.setSeverity(pastIssue.getSeverity());
+      }
+
       newIssue.setCreatedAt(pastIssue.getCreatedAt());
       newIssue.setUpdatedAt(project.getAnalysisDate());
-      newIssue.setKey(pastIssue.getUuid());
+      newIssue.setNew(false);
+
       // TODO
 //      newIssue.setPersonId(pastIssue.getPersonId());
-      newIssue.setNew(false);
 
       lastIssuesByRule.remove(getRule(newIssue), pastIssue);
       issueMap.put(newIssue, pastIssue);
index d29503f9ef0302513c478034945497b9e1fecf2c..4ddea22f1a964885f7571d8e733e8592e19b867a 100644 (file)
@@ -22,8 +22,6 @@ package org.sonar.plugins.core.sensors;
 import com.google.common.collect.Multiset;
 import com.google.common.collect.TreeMultiset;
 import org.sonar.api.CoreProperties;
-import org.sonar.api.Properties;
-import org.sonar.api.Property;
 import org.sonar.api.batch.Decorator;
 import org.sonar.api.batch.DecoratorContext;
 import org.sonar.api.batch.DependedUpon;
@@ -42,16 +40,6 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
-@Properties(
-    @Property(
-        key = CoreProperties.CORE_RULE_WEIGHTS_PROPERTY,
-        defaultValue = CoreProperties.CORE_RULE_WEIGHTS_DEFAULT_VALUE,
-        name = "Rules weight",
-        description = "A weight is associated to each severity to calculate the Rules Compliance Index.",
-        project = false,
-        global = true,
-        category = CoreProperties.CATEGORY_GENERAL)
-)
 public class WeightedViolationsDecorator implements Decorator {
 
   private Settings settings;
index 7b218e8930d0d73ce6146eec1ceb33485f1f7fbb..f5ac91b529297e1a76c66f165dc0e6e5e54569f9 100644 (file)
@@ -200,6 +200,15 @@ public class IssueTrackingDecoratorTest {
     assertThat(newIssue.isNew()).isTrue();
   }
 
+  @Test
+  public void should_set_severity_if_severity_has_been_changed_by_user() {
+    DefaultIssue newIssue = newDefaultIssue("message", 1, "repoKey", "ruleKey", "checksum").setSeverity("MAJOR");
+    IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum").setSeverity("MINOR").setManualSeverity(true);
+
+    Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
+    assertThat(newIssue.severity()).isEqualTo("MINOR");
+  }
+
   @Test
   public void should_copy_date_when_not_new() {
     DefaultIssue newIssue = newDefaultIssue("message", 1, "repoKey", "ruleKey", "checksum");
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/IssuesDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/issue/IssuesDecorator.java
new file mode 100644 (file)
index 0000000..3f2ed8f
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multiset;
+import org.sonar.api.batch.*;
+import org.sonar.api.component.ResourcePerspectives;
+import org.sonar.api.issue.Issuable;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.measures.*;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.rules.RulePriority;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+@DependsUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
+public class IssuesDecorator implements Decorator {
+
+  private final ResourcePerspectives perspectives;
+  private final RuleFinder rulefinder;
+
+  public IssuesDecorator(ResourcePerspectives perspectives, RuleFinder rulefinder) {
+    this.perspectives = perspectives;
+    this.rulefinder = rulefinder;
+  }
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return true;
+  }
+
+  @DependedUpon
+  public List<Metric> generatesIssuesMetrics() {
+    return Arrays.asList(CoreMetrics.ISSUES,
+      CoreMetrics.BLOCKER_ISSUES,
+      CoreMetrics.CRITICAL_ISSUES,
+      CoreMetrics.MAJOR_ISSUES,
+      CoreMetrics.MINOR_ISSUES,
+      CoreMetrics.INFO_ISSUES);
+  }
+
+  public void decorate(Resource resource, DecoratorContext context) {
+    Issuable issuable = perspectives.as(Issuable.class, context.getResource());
+    Collection<Issue> issues = issuable.issues();
+    computeTotalIssues(context, issues);
+    computeIssuesPerSeverities(context, issues);
+    computeIssuesPerRules(context, issues);
+  }
+
+  private void computeTotalIssues(DecoratorContext context, Collection<Issue> issues) {
+    if (context.getMeasure(CoreMetrics.ISSUES) == null) {
+      Collection<Measure> childrenIssues = context.getChildrenMeasures(CoreMetrics.ISSUES);
+      Double sum = MeasureUtils.sum(true, childrenIssues);
+      context.saveMeasure(CoreMetrics.ISSUES, sum + issues.size());
+    }
+  }
+
+  private void computeIssuesPerSeverities(DecoratorContext context, Collection<Issue> issues) {
+    Multiset<RulePriority> severitiesBag = HashMultiset.create();
+    for (Issue issue : issues) {
+      severitiesBag.add(RulePriority.valueOf(issue.severity()));
+    }
+
+    for (RulePriority ruleSeverity : RulePriority.values()) {
+      Metric metric = SeverityUtils.severityToIssueMetric(ruleSeverity);
+      if (context.getMeasure(metric) == null) {
+        Collection<Measure> children = context.getChildrenMeasures(MeasuresFilters.metric(metric));
+        int sum = MeasureUtils.sum(true, children).intValue() + severitiesBag.count(ruleSeverity);
+        context.saveMeasure(metric, (double) sum);
+      }
+    }
+  }
+
+  private void computeIssuesPerRules(DecoratorContext context, Collection<Issue> issues) {
+    Map<RulePriority, Multiset<Rule>> rulesPerSeverity = Maps.newHashMap();
+    for (Issue issue : issues) {
+      Multiset<Rule> rulesBag = initRules(rulesPerSeverity, RulePriority.valueOf(issue.severity()));
+      rulesBag.add(rulefinder.findByKey(issue.ruleRepositoryKey(), issue.ruleKey()));
+    }
+
+    for (RulePriority severity : RulePriority.values()) {
+      Metric metric = SeverityUtils.severityToIssueMetric(severity);
+
+      Collection<Measure> children = context.getChildrenMeasures(MeasuresFilters.rules(metric));
+      for (Measure child : children) {
+        RuleMeasure childRuleMeasure = (RuleMeasure) child;
+        Rule rule = childRuleMeasure.getRule();
+        if (rule != null && MeasureUtils.hasValue(childRuleMeasure)) {
+          Multiset<Rule> rulesBag = initRules(rulesPerSeverity, severity);
+          rulesBag.add(rule, childRuleMeasure.getIntValue());
+        }
+      }
+
+      Multiset<Rule> rulesBag = rulesPerSeverity.get(severity);
+      if (rulesBag != null) {
+        for (Multiset.Entry<Rule> entry : rulesBag.entrySet()) {
+          RuleMeasure measure = RuleMeasure.createForRule(metric, entry.getElement(), (double) entry.getCount());
+          measure.setSeverity(severity);
+          context.saveMeasure(measure);
+        }
+      }
+    }
+  }
+
+  private Multiset<Rule> initRules(Map<RulePriority, Multiset<Rule>> rulesPerSeverity, RulePriority severity) {
+    Multiset<Rule> rulesBag = rulesPerSeverity.get(severity);
+    if (rulesBag == null) {
+      rulesBag = HashMultiset.create();
+      rulesPerSeverity.put(severity, rulesBag);
+    }
+    return rulesBag;
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/IssuesDensityDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/issue/IssuesDensityDecorator.java
new file mode 100644 (file)
index 0000000..ae6e370
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.MeasureUtils;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class IssuesDensityDecorator implements Decorator {
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return true;
+  }
+
+  @DependsUpon
+  public List<Metric> dependsUponWeightedIissuesAndNcloc() {
+    return Arrays.asList(CoreMetrics.WEIGHTED_ISSUES, CoreMetrics.NCLOC);
+  }
+
+  @DependedUpon
+  public Metric generatesIssuesDensity() {
+    return CoreMetrics.ISSUES_DENSITY;
+  }
+
+  public void decorate(Resource resource, DecoratorContext context) {
+    if (shouldDecorateResource(context)) {
+      decorateDensity(context);
+    }
+  }
+
+  protected boolean shouldDecorateResource(DecoratorContext context) {
+    return context.getMeasure(CoreMetrics.ISSUES_DENSITY) == null;
+  }
+
+  private void decorateDensity(DecoratorContext context) {
+    Measure ncloc = context.getMeasure(CoreMetrics.NCLOC);
+    if (MeasureUtils.hasValue(ncloc) && ncloc.getValue() > 0.0) {
+      saveDensity(context, ncloc.getValue().intValue());
+    }
+  }
+
+  private void saveDensity(DecoratorContext context, int ncloc) {
+    Measure debt = context.getMeasure(CoreMetrics.WEIGHTED_ISSUES);
+    Integer debtValue = 0;
+    if (MeasureUtils.hasValue(debt)) {
+      debtValue = debt.getValue().intValue();
+    }
+    double density = calculate(debtValue, ncloc);
+    context.saveMeasure(CoreMetrics.ISSUES_DENSITY, density);
+  }
+
+  protected static double calculate(int debt, int ncloc) {
+    double rci = (1.0 - ((double) debt / (double) ncloc)) * 100.0;
+    rci = Math.max(rci, 0.0);
+    return rci;
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/SeverityUtils.java b/sonar-batch/src/main/java/org/sonar/batch/issue/SeverityUtils.java
new file mode 100644 (file)
index 0000000..7377089
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.issue;
+
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.rules.RulePriority;
+
+final class SeverityUtils {
+  private SeverityUtils() {
+    // only static methods
+  }
+
+  static Metric severityToIssueMetric(RulePriority severity) {
+    Metric metric;
+    if (severity.equals(RulePriority.BLOCKER)) {
+      metric = CoreMetrics.BLOCKER_ISSUES;
+    } else if (severity.equals(RulePriority.CRITICAL)) {
+      metric = CoreMetrics.CRITICAL_ISSUES;
+    } else if (severity.equals(RulePriority.MAJOR)) {
+      metric = CoreMetrics.MAJOR_ISSUES;
+    } else if (severity.equals(RulePriority.MINOR)) {
+      metric = CoreMetrics.MINOR_ISSUES;
+    } else if (severity.equals(RulePriority.INFO)) {
+      metric = CoreMetrics.INFO_ISSUES;
+    } else {
+      throw new IllegalArgumentException("Unsupported severity: " + severity);
+    }
+    return metric;
+  }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/WeightedIssuesDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/issue/WeightedIssuesDecorator.java
new file mode 100644 (file)
index 0000000..031d69b
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.collect.Multiset;
+import com.google.common.collect.TreeMultiset;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.Properties;
+import org.sonar.api.Property;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.MeasureUtils;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.api.utils.KeyValueFormat;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@Properties(
+    @Property(
+        key = CoreProperties.CORE_RULE_WEIGHTS_PROPERTY,
+        defaultValue = CoreProperties.CORE_RULE_WEIGHTS_DEFAULT_VALUE,
+        name = "Rules weight",
+        description = "A weight is associated to each severity to calculate the Rules Compliance Index.",
+        project = false,
+        global = true,
+        category = CoreProperties.CATEGORY_GENERAL)
+)
+public class WeightedIssuesDecorator implements Decorator {
+
+  private Settings settings;
+  private Map<RulePriority, Integer> weightsBySeverity;
+
+  public WeightedIssuesDecorator(Settings settings) {
+    this.settings = settings;
+  }
+
+  @DependsUpon
+  public List<Metric> dependsUponIssues() {
+    return Arrays.asList(CoreMetrics.BLOCKER_ISSUES, CoreMetrics.CRITICAL_ISSUES,
+        CoreMetrics.MAJOR_ISSUES, CoreMetrics.MINOR_ISSUES, CoreMetrics.INFO_ISSUES);
+  }
+
+  @DependedUpon
+  public Metric generatesWeightedIssues() {
+    return CoreMetrics.WEIGHTED_ISSUES;
+  }
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return true;
+  }
+
+  public void start() {
+    weightsBySeverity = getWeights(settings);
+  }
+
+  Map<RulePriority, Integer> getWeightsBySeverity() {
+    return weightsBySeverity;
+  }
+
+  static Map<RulePriority, Integer> getWeights(final Settings settings) {
+    String value = settings.getString(CoreProperties.CORE_RULE_WEIGHTS_PROPERTY);
+
+    Map<RulePriority, Integer> weights = KeyValueFormat.parse(value, KeyValueFormat.newPriorityConverter(), KeyValueFormat.newIntegerConverter());
+
+    for (RulePriority priority : RulePriority.values()) {
+      if (!weights.containsKey(priority)) {
+        weights.put(priority, 1);
+      }
+    }
+    return weights;
+  }
+
+
+  public void decorate(Resource resource, DecoratorContext context) {
+    decorate(context);
+  }
+
+  void decorate(DecoratorContext context) {
+    double debt = 0.0;
+    Multiset<RulePriority> distribution = TreeMultiset.create();
+
+    for (RulePriority severity : RulePriority.values()) {
+      Measure measure = context.getMeasure(SeverityUtils.severityToIssueMetric(severity));
+      if (measure != null && MeasureUtils.hasValue(measure)) {
+        distribution.add(severity, measure.getIntValue());
+        double add = weightsBySeverity.get(severity) * measure.getIntValue();
+        debt += add;
+      }
+    }
+
+    Measure debtMeasure = new Measure(CoreMetrics.WEIGHTED_ISSUES, debt, KeyValueFormat.format(distribution));
+    context.saveMeasure(debtMeasure);
+  }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/IssuesDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/IssuesDecoratorTest.java
new file mode 100644 (file)
index 0000000..9d7697b
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.issue;
+
+import com.google.common.collect.Lists;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.component.ResourcePerspectives;
+import org.sonar.api.issue.Issuable;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.MeasuresFilter;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.api.test.IsRuleMeasure;
+import org.sonar.core.issue.DefaultIssue;
+
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyDouble;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+public class IssuesDecoratorTest {
+
+  private Rule ruleA1;
+  private Rule ruleA2;
+  private Rule ruleB1;
+  private IssuesDecorator decorator;
+  private Resource resource;
+  private DecoratorContext context;
+  private Issuable issuable;
+  private RuleFinder rulefinder;
+
+  @Before
+  public void before() {
+    ruleA1 = Rule.create().setRepositoryKey("ruleA1").setKey("ruleA1").setName("nameA1");
+    ruleA2 = Rule.create().setRepositoryKey("ruleA2").setKey("ruleA2").setName("nameA2");
+    ruleB1 = Rule.create().setRepositoryKey("ruleB1").setKey("ruleB1").setName("nameB1");
+
+    rulefinder = mock(RuleFinder.class);
+    when(rulefinder.findByKey(ruleA1.getRepositoryKey(), ruleA1.getKey())).thenReturn(ruleA1);
+    when(rulefinder.findByKey(ruleA2.getRepositoryKey(), ruleA2.getKey())).thenReturn(ruleA2);
+    when(rulefinder.findByKey(ruleB1.getRepositoryKey(), ruleB1.getKey())).thenReturn(ruleB1);
+
+    resource = mock(Resource.class);
+    context = mock(DecoratorContext.class);
+    when(context.getResource()).thenReturn(resource);
+
+    issuable = mock(Issuable.class);
+    ResourcePerspectives perspectives = mock(ResourcePerspectives.class);
+    when(perspectives.as(Issuable.class, resource)).thenReturn(issuable);
+    decorator = new IssuesDecorator(perspectives, rulefinder);
+  }
+
+  @Test
+  public void should_count_issues() {
+    when(resource.getScope()).thenReturn(Scopes.PROJECT);
+    when(issuable.issues()).thenReturn(createIssues());
+    when(context.getChildrenMeasures(any(MeasuresFilter.class))).thenReturn(Lists.<Measure>newArrayList());
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ISSUES, 4.0);
+  }
+
+  /**
+   * See http://jira.codehaus.org/browse/SONAR-1729
+   */
+  @Test
+  public void should_not_count_issues_if_measure_already_exists() {
+    when(resource.getScope()).thenReturn(Scopes.PROJECT);
+    when(issuable.issues()).thenReturn(createIssues());
+    when(context.getChildrenMeasures(any(MeasuresFilter.class))).thenReturn(Lists.<Measure>newArrayList());
+    when(context.getMeasure(CoreMetrics.ISSUES)).thenReturn(new Measure(CoreMetrics.ISSUES, 3000.0));
+    when(context.getMeasure(CoreMetrics.MAJOR_ISSUES)).thenReturn(new Measure(CoreMetrics.MAJOR_ISSUES, 500.0));
+
+    decorator.decorate(resource, context);
+
+    verify(context, never()).saveMeasure(eq(CoreMetrics.ISSUES), anyDouble());// not changed
+    verify(context, never()).saveMeasure(eq(CoreMetrics.MAJOR_ISSUES), anyDouble());// not changed
+    verify(context, times(1)).saveMeasure(eq(CoreMetrics.CRITICAL_ISSUES), anyDouble());// did not exist
+  }
+
+  @Test
+  public void should_save_zero_on_projects() {
+    when(resource.getScope()).thenReturn(Scopes.PROJECT);
+    when(issuable.issues()).thenReturn(Lists.<Issue>newArrayList());
+    when(context.getChildrenMeasures(any(MeasuresFilter.class))).thenReturn(Lists.<Measure>newArrayList());
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ISSUES, 0.0);
+  }
+
+  @Test
+  public void should_save_zero_on_directories() {
+    when(resource.getScope()).thenReturn(Scopes.DIRECTORY);
+    when(issuable.issues()).thenReturn(Lists.<Issue>newArrayList());
+    when(context.getChildrenMeasures(any(MeasuresFilter.class))).thenReturn(Lists.<Measure>newArrayList());
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ISSUES, 0.0);
+  }
+
+  @Test
+  public void should_count_issues_by_severity() {
+    when(resource.getScope()).thenReturn(Scopes.PROJECT);
+    when(issuable.issues()).thenReturn(createIssues());
+    when(context.getChildrenMeasures(any(MeasuresFilter.class))).thenReturn(Lists.<Measure>newArrayList());
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.BLOCKER_ISSUES, 0.0);
+    verify(context).saveMeasure(CoreMetrics.CRITICAL_ISSUES, 2.0);
+    verify(context).saveMeasure(CoreMetrics.MAJOR_ISSUES, 1.0);
+    verify(context).saveMeasure(CoreMetrics.MINOR_ISSUES, 1.0);
+    verify(context).saveMeasure(CoreMetrics.INFO_ISSUES, 0.0);
+  }
+
+  @Test
+  public void should_count_issues_per_rule() {
+    List<Issue> issues = newArrayList();
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA1.getRepositoryKey()).setRuleKey(ruleA1.getKey()).setSeverity(RulePriority.CRITICAL.name()));
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA1.getRepositoryKey()).setRuleKey(ruleA1.getKey()).setSeverity(RulePriority.CRITICAL.name()));
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA2.getRepositoryKey()).setRuleKey(ruleA2.getKey()).setSeverity(RulePriority.MAJOR.name()));
+    when(issuable.issues()).thenReturn(issues);
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(argThat(new IsRuleMeasure(CoreMetrics.CRITICAL_ISSUES, ruleA1, 2.0)));
+    verify(context, never()).saveMeasure(argThat(new IsRuleMeasure(CoreMetrics.MAJOR_ISSUES, ruleA1, 0.0)));
+    verify(context).saveMeasure(argThat(new IsRuleMeasure(CoreMetrics.MAJOR_ISSUES, ruleA2, 1.0)));
+  }
+
+  @Test
+  public void same_rule_should_have_different_severities() {
+    List<Issue> issues = newArrayList();
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA1.getRepositoryKey()).setRuleKey(ruleA1.getKey()).setSeverity(RulePriority.CRITICAL.name()));
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA1.getRepositoryKey()).setRuleKey(ruleA1.getKey()).setSeverity(RulePriority.CRITICAL.name()));
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA1.getRepositoryKey()).setRuleKey(ruleA1.getKey()).setSeverity(RulePriority.MINOR.name()));
+    when(issuable.issues()).thenReturn(issues);
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(argThat(new IsRuleMeasure(CoreMetrics.CRITICAL_ISSUES, ruleA1, 2.0)));
+    verify(context).saveMeasure(argThat(new IsRuleMeasure(CoreMetrics.MINOR_ISSUES, ruleA1, 1.0)));
+  }
+
+  private List<Issue> createIssues() {
+    List<Issue> issues = newArrayList();
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA1.getRepositoryKey()).setRuleKey(ruleA1.getKey()).setSeverity(RulePriority.CRITICAL.name()));
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA1.getRepositoryKey()).setRuleKey(ruleA1.getKey()).setSeverity(RulePriority.CRITICAL.name()));
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleA2.getRepositoryKey()).setRuleKey(ruleA2.getKey()).setSeverity(RulePriority.MAJOR.name()));
+    issues.add(new DefaultIssue().setRuleRepositoryKey(ruleB1.getRepositoryKey()).setRuleKey(ruleB1.getKey()).setSeverity(RulePriority.MINOR.name()));
+    return issues;
+  }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/IssuesDensityDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/IssuesDensityDecoratorTest.java
new file mode 100644 (file)
index 0000000..1360032
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.issue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.Scopes;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+public class IssuesDensityDecoratorTest {
+
+  private IssuesDensityDecorator decorator;
+  private Resource resource;
+
+  @Before
+  public void before() {
+    resource = mock(Resource.class);
+    when(resource.getScope()).thenReturn(Scopes.PROJECT);
+    decorator = new IssuesDensityDecorator();
+  }
+
+  @Test
+  public void calculate_density() {
+    assertThat(IssuesDensityDecorator.calculate(4000, 200)).isEqualTo(0.0);
+    assertThat(IssuesDensityDecorator.calculate(200, 200)).isEqualTo(0.0);
+    assertThat(IssuesDensityDecorator.calculate(50, 200)).isEqualTo(75.0);
+    assertThat(IssuesDensityDecorator.calculate(0, 200)).isEqualTo(100.0);
+  }
+
+  @Test
+  public void decorate_density() {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 200.0));
+    when(context.getMeasure(CoreMetrics.WEIGHTED_ISSUES)).thenReturn(new Measure(CoreMetrics.WEIGHTED_ISSUES, 50.0));
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ISSUES_DENSITY, 75.0);
+  }
+
+  @Test
+  public void no_density_if_no_ncloc() {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 0.0));
+    when(context.getMeasure(CoreMetrics.WEIGHTED_ISSUES)).thenReturn(new Measure(CoreMetrics.WEIGHTED_ISSUES, 50.0));
+
+    decorator.decorate(resource, context);
+
+    verify(context, never()).saveMeasure(eq(CoreMetrics.ISSUES_DENSITY), anyDouble());
+  }
+
+  @Test
+  public void save_density_if_value_is_zero() {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 200.0));
+    when(context.getMeasure(CoreMetrics.WEIGHTED_ISSUES)).thenReturn(new Measure(CoreMetrics.WEIGHTED_ISSUES, 5000.0));
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ISSUES_DENSITY, 0.0);
+  }
+
+  @Test
+  public void density_is_hundred_when_no_debt() {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 200.0));
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ISSUES_DENSITY, 100.0);
+  }
+
+  @Test
+  public void density_is_hundred_when_debt_is_zero() {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 200.0));
+    when(context.getMeasure(CoreMetrics.WEIGHTED_ISSUES)).thenReturn(new Measure(CoreMetrics.WEIGHTED_ISSUES, 0.0));
+
+    decorator.decorate(resource, context);
+
+    verify(context).saveMeasure(CoreMetrics.ISSUES_DENSITY, 100.0);
+  }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/WeightedIssuesDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/WeightedIssuesDecoratorTest.java
new file mode 100644 (file)
index 0000000..6d333ca
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.issue;
+
+import org.hamcrest.core.Is;
+import org.junit.Test;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.rules.RulePriority;
+import org.sonar.api.test.IsMeasure;
+
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.*;
+
+public class WeightedIssuesDecoratorTest {
+
+  @Test
+  public void test_weighted_issues() {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.CORE_RULE_WEIGHTS_PROPERTY, "BLOCKER=10;CRITICAL=5;MAJOR=2;MINOR=1;INFO=0");
+    WeightedIssuesDecorator decorator = new WeightedIssuesDecorator(settings);
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.INFO_ISSUES)).thenReturn(new Measure(CoreMetrics.INFO_ISSUES, 50.0));
+    when(context.getMeasure(CoreMetrics.CRITICAL_ISSUES)).thenReturn(new Measure(CoreMetrics.CRITICAL_ISSUES, 80.0));
+    when(context.getMeasure(CoreMetrics.BLOCKER_ISSUES)).thenReturn(new Measure(CoreMetrics.BLOCKER_ISSUES, 100.0));
+
+    decorator.start();
+    decorator.decorate(context);
+
+    verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.WEIGHTED_ISSUES, (double) (100 * 10 + 80 * 5 + 50 * 0))));
+    verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.WEIGHTED_ISSUES, "INFO=50;CRITICAL=80;BLOCKER=100")));
+  }
+
+  // SONAR-3092
+  @Test
+  public void do_save_zero() {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.CORE_RULE_WEIGHTS_PROPERTY, "BLOCKER=10;CRITICAL=5;MAJOR=2;MINOR=1;INFO=0");
+    DecoratorContext context = mock(DecoratorContext.class);
+
+    WeightedIssuesDecorator decorator = new WeightedIssuesDecorator(settings);
+    decorator.start();
+    decorator.decorate(context);
+
+    verify(context).saveMeasure(any(Measure.class));
+  }
+
+  @Test
+  public void should_load_severity_weights_at_startup() {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.CORE_RULE_WEIGHTS_PROPERTY, "BLOCKER=2;CRITICAL=1;MAJOR=0;MINOR=0;INFO=0");
+    
+    WeightedIssuesDecorator decorator = new WeightedIssuesDecorator(settings);
+    decorator.start();
+
+    assertThat(decorator.getWeightsBySeverity().get(RulePriority.BLOCKER), Is.is(2));
+    assertThat(decorator.getWeightsBySeverity().get(RulePriority.CRITICAL), Is.is(1));
+    assertThat(decorator.getWeightsBySeverity().get(RulePriority.MAJOR), Is.is(0));
+  }
+
+  @Test
+  public void weights_setting_should_be_optional() {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.CORE_RULE_WEIGHTS_PROPERTY, "BLOCKER=2");
+
+    WeightedIssuesDecorator decorator = new WeightedIssuesDecorator(settings);
+    decorator.start();
+
+    assertThat(decorator.getWeightsBySeverity().get(RulePriority.MAJOR), Is.is(1));
+  }
+}
\ No newline at end of file
index 85266b8afd317ba4c70630811b8b96f0685aa0f8..ef463a683ffb21801a087db34725b78a78afe350 100644 (file)
@@ -1526,6 +1526,156 @@ public final class CoreMetrics {
       .setDeleteHistoricalData(true)
       .create();
 
+  // --------------------------------------------------------------------------------------------------------------------
+  //
+  // ISSUES
+  //
+  // --------------------------------------------------------------------------------------------------------------------  
+
+  public static final String ISSUES_KEY = "issues";
+  public static final Metric ISSUES = new Metric.Builder(ISSUES_KEY, "Issues", Metric.ValueType.INT)
+      .setDescription("Issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .create();
+
+  public static final String WEIGHTED_ISSUES_KEY = "weighted_issues";
+  public static final Metric WEIGHTED_ISSUES = new Metric.Builder(WEIGHTED_ISSUES_KEY, "Weighted issues", Metric.ValueType.INT)
+      .setDescription("Weighted issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .create();
+
+  public static final String ISSUES_DENSITY_KEY = "issues_density";
+  public static final Metric ISSUES_DENSITY = new Metric.Builder(ISSUES_DENSITY_KEY, "Rules compliance", Metric.ValueType.PERCENT)
+      .setDescription("Rules compliance")
+      .setDirection(Metric.DIRECTION_BETTER)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .create();
+
+  public static final String BLOCKER_ISSUES_KEY = "blocker_issues";
+  public static final Metric BLOCKER_ISSUES = new Metric.Builder(BLOCKER_ISSUES_KEY, "Blocker issues", Metric.ValueType.INT)
+      .setDescription("Blocker issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .create();
+
+  public static final String CRITICAL_ISSUES_KEY = "critical_issues";
+  public static final Metric CRITICAL_ISSUES = new Metric.Builder(CRITICAL_ISSUES_KEY, "Critical issues", Metric.ValueType.INT)
+      .setDescription("Critical issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .create();
+
+  public static final String MAJOR_ISSUES_KEY = "major_issues";
+  public static final Metric MAJOR_ISSUES = new Metric.Builder(MAJOR_ISSUES_KEY, "Major issues", Metric.ValueType.INT)
+      .setDescription("Major issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .create();
+
+  public static final String MINOR_ISSUES_KEY = "minor_issues";
+  public static final Metric MINOR_ISSUES = new Metric.Builder(MINOR_ISSUES_KEY, "Minor issues", Metric.ValueType.INT)
+      .setDescription("Minor issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .create();
+
+  public static final String INFO_ISSUES_KEY = "info_issues";
+  public static final Metric INFO_ISSUES = new Metric.Builder(INFO_ISSUES_KEY, "Info issues", Metric.ValueType.INT)
+      .setDescription("Info issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .create();
+
+  public static final String NEW_ISSUES_KEY = "new_issues";
+  public static final Metric NEW_ISSUES = new Metric.Builder(NEW_ISSUES_KEY, "New issues", Metric.ValueType.INT)
+      .setDescription("New issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setDeleteHistoricalData(true)
+      .create();
+
+  public static final String NEW_BLOCKER_ISSUES_KEY = "new_blocker_issues";
+  public static final Metric NEW_BLOCKER_ISSUES = new Metric.Builder(NEW_BLOCKER_ISSUES_KEY, "New Blocker issues", Metric.ValueType.INT)
+      .setDescription("New Blocker issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setDeleteHistoricalData(true)
+      .create();
+
+  public static final String NEW_CRITICAL_ISSUES_KEY = "new_critical_issues";
+  public static final Metric NEW_CRITICAL_ISSUES = new Metric.Builder(NEW_CRITICAL_ISSUES_KEY, "New Critical issues", Metric.ValueType.INT)
+      .setDescription("New Critical issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setDeleteHistoricalData(true)
+      .create();
+
+  public static final String NEW_MAJOR_ISSUES_KEY = "new_major_issues";
+  public static final Metric NEW_MAJOR_ISSUES = new Metric.Builder(NEW_MAJOR_ISSUES_KEY, "New Major issues", Metric.ValueType.INT)
+      .setDescription("New Major issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setDeleteHistoricalData(true)
+      .create();
+
+  public static final String NEW_MINOR_ISSUES_KEY = "new_minor_issues";
+  public static final Metric NEW_MINOR_ISSUES = new Metric.Builder(NEW_MINOR_ISSUES_KEY, "New Minor issues", Metric.ValueType.INT)
+      .setDescription("New Minor issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setDeleteHistoricalData(true)
+      .create();
+
+  public static final String NEW_INFO_ISSUES_KEY = "new_info_issues";
+  public static final Metric NEW_INFO_ISSUES = new Metric.Builder(NEW_INFO_ISSUES_KEY, "New Info issues", Metric.ValueType.INT)
+      .setDescription("New Info issues")
+      .setDirection(Metric.DIRECTION_WORST)
+      .setQualitative(true)
+      .setDomain(DOMAIN_RULES)
+      .setBestValue(0.0)
+      .setOptimizedBestValue(true)
+      .setDeleteHistoricalData(true)
+      .create();  
+  
   // --------------------------------------------------------------------------------------------------------------------
   //
   // DESIGN
index 7dc65fdfdca1ff291781dd1361bd16e6b7d840c5..09c5d06e50c4be2dfa0626d07922f4549c73ed2d 100644 (file)
@@ -193,4 +193,6 @@ public class Issue extends Model {
         .append(key)
         .toString();
   }
+
+
 }
index 0f01d4d2b976817189c2ca3e17f420d0a644955f..d3424d7dcfca089ebaa5bab23f7c9fdc064745e8 100644 (file)
@@ -30,8 +30,8 @@ public class IssueUnmarshaller extends AbstractUnmarshaller<Issue> {
     return new Issue()
         .setKey(utils.getString(json, "key"))
         .setComponentKey(utils.getString(json, "component"))
-        .setRuleKey(utils.getString(json, "ruleKey"))
-        .setRuleRepositoryKey(utils.getString(json, "ruleRepositoryKey"))
+        .setRuleKey(utils.getString(json, "rule"))
+        .setRuleRepositoryKey(utils.getString(json, "ruleRepository"))
         .setSeverity(utils.getString(json, "severity"))
         .setTitle(utils.getString(json, "title"))
         .setMessage(utils.getString(json, "message"))
index 5d5f2745dcf88492797c253bb0f6ae26b91be861..113e1e72fbd780e843625f0130b85247c6ffd605 100644 (file)
@@ -2,8 +2,8 @@
   {
     "key": "029d283a-072b-4ef8-bdda-e4b212aa39e3",
     "component": "com.sonarsource.it.samples:simple-sample:sample",
-    "ruleKey": "NM_FIELD_NAMING_CONVENTION",
-    "ruleRepositoryKey": "findbugs",
+    "rule": "NM_FIELD_NAMING_CONVENTION",
+    "ruleRepository": "findbugs",
     "severity": "MAJOR",
     "title": "title",
     "message": "message",